Skip to content

Conversation

@LesiaUKR
Copy link
Collaborator

@LesiaUKR LesiaUKR commented Jan 3, 2026

Closes Issue #106 #107 #108 #109 #111 #112
Week 4
• Quiz performance:

  • Eliminate N+1 queries in getQuizQuestions by batch-loading answers
  • Optimize submitQuizAttempt with inArray-based batch verification
  • Reduce quiz page load from 21 queries to 2
  • Add seed to randomizeQuizQuestions for stable randomization
  • Handle empty answers array and validate answer–question matching

• Points system refactor (ledger-only):

  • Migrate to ledger-based points model (point_transactions as source of truth)
  • Remove users.points column and db.transaction() usage (neon-http compatible)
  • Store full attempt score in quiz_attempts.pointsEarned
  • Award only delta vs previous best attempt
  • Compute total points via SUM(point_transactions.points)
  • Optimize previous best lookup using MAX(pointsEarned)

• Quiz content & seeding:

  • Add seed scripts for HTML, CSS, Git, JavaScript, TypeScript quizzes
  • Introduce advanced and fundamentals quiz trackse

• UI & logic fixes:

  • Add pointsAwarded prop to QuizResult
  • Show "+N points added" or "no points (result not improved)"
  • Fix Dashboard to rely on ledger total

• Leaderboard & DB performance:

  • Update leaderboard queries to LEFT JOIN + GROUP BY + SUM
  • Add index for point_transactions (user_id)
  • Apply drizzle migrations and snapshots

Benefits:

  • Faster quiz loads and lower DB pressure
  • Neon-compatible (no transactions)

Summary by CodeRabbit

  • New Features

    • Show earned points in quiz results and persist points for attempts.
    • Many new quizzes added (TypeScript, CSS, HTML, Git — fundamentals & advanced).
    • Improved quiz randomization with seeded behavior.
  • Bug Fixes

    • Stronger validation for quiz submissions and guest/registered session checks.
    • Improved signup error handling and standardized error responses.
    • Safer time handling for quiz attempts.
  • Performance

    • More accurate countdown timer.
    • Leaderboard cache refreshed less frequently.
    • Backend query batching for faster quiz operations.

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

• Quiz performance:
  - Eliminate N+1 queries in getQuizQuestions by batch-loading answers
  - Optimize submitQuizAttempt with inArray-based batch verification
  - Reduce quiz page load from 21 queries to 2
  - Add seed to randomizeQuizQuestions for stable randomization
  - Handle empty answers array and validate answer–question matching

• Points system refactor (ledger-only):
  - Migrate to ledger-based points model (point_transactions as source of truth)
  - Remove users.points column and db.transaction() usage (neon-http compatible)
  - Store full attempt score in quiz_attempts.pointsEarned
  - Award only delta vs previous best attempt
  - Compute total points via SUM(point_transactions.points)
  - Optimize previous best lookup using MAX(pointsEarned)

• Quiz content & seeding:
  - Add seed scripts for HTML, CSS, Git, JavaScript, TypeScript quizzes
  - Introduce advanced and fundamentals quiz trackse

• UI & logic fixes:
  - Add pointsAwarded prop to QuizResult
  - Show "+N points added" or "no points (result not improved)"
  - Fix Dashboard to rely on ledger total

• Leaderboard & DB performance:
  - Update leaderboard queries to LEFT JOIN + GROUP BY + SUM
  - Add index for point_transactions (user_id)
  - Apply drizzle migrations and snapshots

Benefits:
- Faster quiz loads and lower DB pressure
- Neon-compatible (no transactions)
@netlify
Copy link

netlify bot commented Jan 3, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit bc9a153
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/695878cc59595d0008993f30
😎 Deploy Preview https://deploy-preview-104--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

coderabbitai bot commented Jan 3, 2026

📝 Walkthrough

Walkthrough

Added transactional points via pointTransactions and moved user points out of users. Strengthened quiz submission and guest-result handlers with comprehensive validation, batch fetching, and pointsEarned calculation/storage. Refactored quiz question/answer fetching for batch queries. Improved CountdownTimer accuracy and added many multilingual quiz seed scripts and data.

Changes

Cohort / File(s) Summary
Quiz actions & API
frontend/actions/quiz.ts, frontend/app/api/quiz/guest-result/route.ts
Added totalQuestions propagation; strict session/user checks; comprehensive payload validation (counts, duplicates, question/answer integrity); batch-fetch questions/answers; compute pointsEarned via calculateQuizPoints; persist attempt and per-question answers; standardized error responses.
UI: quiz components & pages
frontend/components/quiz/CountdownTimer.tsx, frontend/components/quiz/QuizContainer.tsx, frontend/components/quiz/QuizResult.tsx, frontend/app/[locale]/quiz/[slug]/page.tsx
CountdownTimer uses fixed endTime + 100ms interval and visibility refresh; QuizContainer adds pointsAwarded state and includes totalQuestions in submissions; QuizResult accepts answeredCount and pointsAwarded and shows new messages/UI; quiz page passes a seed to randomized question fetch.
Pages: dashboard & leaderboard
frontend/app/[locale]/dashboard/page.tsx, frontend/app/[locale]/leaderboard/page.tsx
Dashboard: stop aggregating realPoints from attempts (use user points). Leaderboard: increased revalidate from 300→3600s.
DB schema & migrations
frontend/db/schema/quiz.ts, frontend/db/schema/users.ts, frontend/drizzle/0010_parallel_princess_powerful.sql, frontend/drizzle/0009_unknown_nico_minoru.sql, frontend/drizzle/meta/*
Added pointsEarned to quizAttempts; removed points from users; migration snapshots and index/constraint changes included.
DB queries: quiz, points, leaderboard, users
frontend/db/queries/quiz.ts, frontend/db/queries/points.ts, frontend/db/queries/leaderboard.ts, frontend/db/queries/users.ts
getQuizQuestions refactored to two-query batch answers approach; getBestPreviousPoints simplified to aggregate MAX(pointsEarned); awardQuizPoints now inserts pointTransactions and revalidates leaderboard tag; leaderboard query aggregates COALESCE(SUM(pointTransactions.points)). getUserProfile now returns computed points total.
Point transactions schema & seed
frontend/db/schema/points.ts, frontend/db/seed-demo-leaderboard.ts
Added index on point_transactions.user_id; demo seeding now deletes existing seed transactions then inserts pointTransactions with metadata.source='seed'.
Auth & proxy
frontend/lib/auth.ts, frontend/proxy.ts
Token payload extended with optional exp; isAuthenticated verifies decoded token and expiry; removed redirect block for auth pages.
Signup route
frontend/app/api/auth/signup/route.ts
Wrapped POST handler in try/catch, added logging and standardized 500 error response on failure.
UI copy
frontend/components/quiz/QuizzesSection.tsx
Changed empty-category message to Ukrainian text.
Seed scripts (many)
frontend/db/seed-quiz-*.ts, json/quizzes/**/seed-quiz-*.ts, frontend/db/seed-quiz-*.ts, frontend/db/seed-quiz-*-advanced.ts, etc.
Added/updated many seed scripts for CSS/HTML/Git/JS/TS quizzes with multi-locale support, per-part seeding, attempt-safety checks, and progress logging.
Quiz data (multilingual JSON)
frontend/parse/typescript/**, json/quizzes/**
Added large sets of quiz JSON data (TypeScript, CSS, HTML, JavaScript, Git) across fundamentals and advanced parts (multiple files), used by seed scripts and quiz pages.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Action as submitQuizAttempt (Action)
    participant DB as Database
    participant Points as PointsCalc

    Client->>Action: Submit quiz (quizId, userId, answers, timeSpent)
    rect rgb(220,235,255)
    Note over Action: Auth & session
    Action->>Action: getCurrentUser()
    Action->>Action: validate userId match
    end

    rect rgb(220,255,235)
    Note over Action: Validation & batch loads
    Action->>DB: Fetch quizQuestions(quizId)
    DB-->>Action: questions[]
    Action->>DB: Fetch quizAnswers(selectedAnswerIds)
    DB-->>Action: answers[]
    Action->>Action: validate counts, duplicates, pairings, time
    end

    rect rgb(255,235,220)
    Note over Action: Scoring
    Action->>Points: calculateQuizPoints(totalQuestions, correctCount)
    Points-->>Action: pointsEarned
    end

    rect rgb(235,255,235)
    Note over Action: Persistence
    Action->>DB: Insert quizAttempts (pointsEarned, metadata...)
    Action->>DB: Insert quizAttemptAnswers (per-question)
    DB-->>Action: OK
    end

    Action-->>Client: Result (score, pointsEarned, totalQuestions)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka
  • kryvosheyin

Poem

🐰 Hop, I tunneled through code tonight,

Points now travel in clear sight,
Questions checked, answers tight,
Timers tick and seeds take flight,
Leaderboards glow — a carrot-bright delight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: performance optimizations for quiz and points systems, along with quiz seeding enhancements.
✨ 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

@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: 17

Caution

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

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

143-157: Both buttons navigate to signup, but one is labeled "Login".

The "Увійти" (Login) button at line 150 navigates to /signup. This appears to be a bug - it should likely navigate to /login instead. Additionally, remove the console.log at line 145.

🔎 Proposed fix
   <Button
-    onClick={() => {
-  const url = `/${locale}/signup?returnTo=/quiz/${quizSlug}`;
-  console.log('Navigating to signup:', url);
-  window.location.href = url;
-}}
+    onClick={() => {
+      window.location.href = `/${locale}/login?returnTo=/quiz/${quizSlug}`;
+    }}
     variant="primary"
   >
     Увійти
   </Button>
🧹 Nitpick comments (15)
frontend/components/quiz/CountdownTimer.tsx (1)

38-51: Consider triggering onTimeUp directly when tab becomes visible and time has expired.

If time expires while the tab is hidden, the visibility handler updates the UI to show 0 but relies on the next interval tick (up to 100ms later) to call onTimeUp. For faster response, call onTimeUp directly when remaining is 0:

Proposed improvement
  const handleVisibilityChange = () => {
    if (!document.hidden) {
      const remaining = Math.max(0, Math.floor((endTime - Date.now()) / 1000));
      setRemainingSeconds(remaining);
+     if (remaining === 0) {
+       queueMicrotask(onTimeUp);
+     }
    }
  };

Note: You'd need to add onTimeUp to the effect's dependency array for this change.

frontend/app/api/auth/signup/route.ts (1)

20-20: Remove debug console.log or use proper logging.

The console.log statement appears to be debug code. Either remove it or replace it with a structured logging solution for production observability.

frontend/db/seed-quiz-html.ts (2)

64-68: Minor: async keyword unnecessary for synchronous file read.

loadQuestions is marked async but uses synchronous readFileSync. While harmless, consider either removing async or using fs/promises.readFile for consistency.

🔎 Option: Use async file read
+import { readFile } from 'fs/promises';
-import { readFileSync } from 'fs';

 async function loadQuestions(partNumber: number): Promise<QuestionData[]> {
   const partPath = join(process.cwd(), 'parse', 'html', 'fundamentals', `html-quiz-part${partNumber}.json`);
-  const partData: QuizPartData = JSON.parse(readFileSync(partPath, 'utf-8'));
+  const partData: QuizPartData = JSON.parse(await readFile(partPath, 'utf-8'));
   return partData.questions;
 }

141-180: Consider batching inserts for better seed performance (optional).

The current implementation performs individual inserts per question, locale, and answer. For a seed script this is acceptable, but if seeding becomes slow with larger datasets, Drizzle supports batch inserts via arrays in .values().

frontend/db/seed-quiz-html-advanced.ts (1)

1-240: Consider extracting shared seeding logic to reduce duplication (optional).

This file shares ~95% of its code with seed-quiz-html.ts and seed-quiz-css.ts. A factory function accepting quiz metadata and paths could consolidate this:

// Potential shared utility
function createQuizSeeder(config: QuizSeedConfig) {
  return { ensureQuizExists, seedQuestions, seedQuizFromJson };
}

This is optional and can be deferred—seed scripts with some duplication are common and maintainable.

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

142-142: LGTM! Consider adding an index for points-based queries (optional).

The pointsEarned column correctly supports the ledger-based points model. If queries like MAX(pointsEarned) WHERE userId = ? AND quizId = ? become frequent, a composite index could improve performance:

userQuizPointsIdx: index('quiz_attempts_user_quiz_points_idx').on(
  table.userId,
  table.quizId,
  table.pointsEarned
),

This is optional—monitor query performance first.

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

37-37: Minor: Redundant fallback in points conversion.

The || 0 fallback is redundant since COALESCE(SUM(...), 0) at line 16 already guarantees a non-null number. The SQL expression will never return NULL.

🔎 Proposed simplification
-        points: Number(u.points) || 0,
+        points: Number(u.points),
frontend/db/queries/quiz.ts (1)

187-226: Clean batch loading implementation.

The in-memory grouping is efficient. One minor observation: the Map type Map<string, typeof allAnswers> works but could be more precise as Map<string, (typeof allAnswers)[number][]> to explicitly indicate it holds arrays of individual answer elements rather than the full query result type.

This is purely a readability nit — the current implementation is correct.

frontend/db/seed-quiz-javascript-advanced.ts (2)

64-68: Function marked async but uses synchronous I/O.

loadQuestions is declared async but only uses synchronous readFileSync. Consider either removing async or switching to fs/promises.readFile for consistency.

🔎 Proposed fix
-async function loadQuestions(partNumber: number): Promise<QuestionData[]> {
-    const partPath = join(process.cwd(), 'parse', 'js', 'advanced', `javascript-advanced-quiz-part${partNumber}.json`);
-    const partData: QuizPartData = JSON.parse(readFileSync(partPath, 'utf-8'));
+function loadQuestions(partNumber: number): QuestionData[] {
+  const partPath = join(process.cwd(), 'parse', 'js', 'advanced', `javascript-advanced-quiz-part${partNumber}.json`);
+  const partData: QuizPartData = JSON.parse(readFileSync(partPath, 'utf-8'));
   return partData.questions;
 }

212-212: Answer count assumption may be incorrect.

The summary assumes exactly 4 answers per question (totalQuestions * 4). If questions have varying answer counts, this will be inaccurate. Consider tracking actual answer counts.

frontend/db/seed-quiz-css-advanced.ts (1)

1-240: High code duplication across quiz seed scripts.

This script is nearly identical to seed-quiz-javascript-advanced.ts and other seed scripts. The only differences are the CATEGORY_SLUG, QUIZ_METADATA, JSON paths, and displayOrder values. Consider extracting a shared seed utility to reduce maintenance burden.

🔎 Example approach
// db/utils/seed-quiz.ts
export interface QuizSeedConfig {
  categorySlug: string;
  slug: string;
  questionsCount: number;
  timeLimitSeconds: number;
  displayOrder: number;
  translations: Record<Locale, { title: string; description: string }>;
  jsonPath: (part: number) => string;
}

export async function seedQuiz(config: QuizSeedConfig, partArg: string) {
  // Shared implementation
}

Then each seed script becomes minimal:

// seed-quiz-css-advanced.ts
import { seedQuiz } from './utils/seed-quiz';

seedQuiz({
  categorySlug: 'css',
  slug: 'css-advanced',
  // ...config
}, process.argv[2]);
frontend/actions/quiz.ts (1)

12-12: Unused import.

The and import from drizzle-orm is not used in the active code.

🔎 Proposed fix
-import { eq, inArray, and } from 'drizzle-orm';
+import { eq, inArray } from 'drizzle-orm';
frontend/db/seed-quiz-git.ts (2)

64-68: Unnecessary async and missing error handling.

The function is marked async but performs only synchronous operations. Additionally, if the file doesn't exist or contains invalid JSON, the error will be cryptic.

🔎 Suggested improvement
-async function loadQuestions(partNumber: number): Promise<QuestionData[]> {
-  const partPath = join(process.cwd(), 'parse', 'git', `git-quiz-part${partNumber}.json`);
-  const partData: QuizPartData = JSON.parse(readFileSync(partPath, 'utf-8'));
-  return partData.questions;
+function loadQuestions(partNumber: number): QuestionData[] {
+  const partPath = join(process.cwd(), 'parse', 'git', `git-quiz-part${partNumber}.json`);
+  try {
+    const partData: QuizPartData = JSON.parse(readFileSync(partPath, 'utf-8'));
+    return partData.questions;
+  } catch (error) {
+    throw new Error(`Failed to load questions from ${partPath}: ${error instanceof Error ? error.message : error}`);
+  }
 }

207-225: Hardcoded answer count assumption in log output.

Lines 210 and 225 assume each question has exactly 4 answers (* 4). If questions have variable answer counts, the logged totals will be inaccurate. Consider computing the actual count.

🔎 Suggested fix for accurate logging

For the "all" branch, accumulate actual answer counts:

       let totalQuestions = 0;
+      let totalAnswers = 0;

       for (let i = 1; i <= 4; i++) {
         const questions = await loadQuestions(i);
         await seedQuestions(questions, quizId, i);
         totalQuestions += questions.length;
+        totalAnswers += questions.reduce((sum, q) => sum + q.answers.length, 0);
       }

       console.log('\nAll parts seeded successfully!');
       console.log(`   - 1 quiz with ${LOCALES.length} translations`);
       console.log(`   - ${totalQuestions} questions total`);
-      console.log(`   - ${totalQuestions * 4} answers with ${LOCALES.length} translations each`);
+      console.log(`   - ${totalAnswers} answers with ${LOCALES.length} translations each`);

Similarly for the single-part branch at line 225.

frontend/app/api/quiz/guest-result/route.ts (1)

231-279: Consider broader error handling coverage.

The try-catch only wraps the persistence operations (lines 231-262), but earlier DB queries at lines 129-132 and 178-185 could also throw. If they fail, the error propagates without the "Failed to save guest quiz result" message.

🔎 Suggested improvement

Move the try-catch to wrap all DB operations:

+  try {
   const questionRows = await db
     .select({ id: quizQuestions.id })
     .from(quizQuestions)
     .where(eq(quizQuestions.quizId, quizId));
   // ... rest of validation and persistence ...
-  try {
     const [attempt] = await db
       .insert(quizAttempts)
       // ...
+  } catch (error) {
+    console.error("Failed to process guest quiz result:", error);
+    return NextResponse.json(
+      { success: false, error: "Failed to process result" },
+      { status: 500 }
+    );
+  }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0858b3 and 863a080.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (47)
  • frontend/actions/quiz.ts
  • frontend/app/[locale]/dashboard/page.tsx
  • frontend/app/[locale]/leaderboard/page.tsx
  • frontend/app/[locale]/quiz/[slug]/page.tsx
  • frontend/app/api/auth/signup/route.ts
  • frontend/app/api/quiz/guest-result/route.ts
  • frontend/components/quiz/CountdownTimer.tsx
  • frontend/components/quiz/QuizContainer.tsx
  • frontend/components/quiz/QuizResult.tsx
  • frontend/components/quiz/QuizzesSection.tsx
  • frontend/db/queries/leaderboard.ts
  • frontend/db/queries/points.ts
  • frontend/db/queries/quiz.ts
  • frontend/db/queries/users.ts
  • frontend/db/schema/points.ts
  • frontend/db/schema/quiz.ts
  • frontend/db/schema/users.ts
  • frontend/db/seed-demo-leaderboard.ts
  • frontend/db/seed-quiz-css-advanced.ts
  • frontend/db/seed-quiz-css.ts
  • frontend/db/seed-quiz-git.ts
  • frontend/db/seed-quiz-html-advanced.ts
  • frontend/db/seed-quiz-html.ts
  • frontend/db/seed-quiz-javascript-advanced.ts
  • frontend/db/seed-quiz-typescript-advanced.ts
  • frontend/db/seed-quiz-typescript.ts
  • frontend/drizzle/0009_unknown_nico_minoru.sql
  • frontend/drizzle/0010_parallel_princess_powerful.sql
  • frontend/drizzle/meta/0009_snapshot.json
  • frontend/drizzle/meta/0010_snapshot.json
  • frontend/drizzle/meta/_journal.json
  • frontend/next.config.ts
  • frontend/parse/javascript-quiz-part1.json
  • frontend/parse/javascript-quiz-part2.json
  • frontend/parse/javascript-quiz-part3.json
  • frontend/parse/javascript-quiz-part4.json
  • frontend/parse/react-quiz-data-part1.json
  • frontend/parse/react-quiz-data-part2.json
  • frontend/parse/typescript/advanced/typescript-advanced-quiz-part1.json
  • frontend/parse/typescript/advanced/typescript-advanced-quiz-part2.json
  • frontend/parse/typescript/advanced/typescript-advanced-quiz-part3.json
  • frontend/parse/typescript/advanced/typescript-advanced-quiz-part4.json
  • frontend/parse/typescript/fundamentals/typescript-quiz-part1.json
  • frontend/parse/typescript/fundamentals/typescript-quiz-part2.json
  • frontend/parse/typescript/fundamentals/typescript-quiz-part3.json
  • frontend/parse/typescript/fundamentals/typescript-quiz-part4.json
  • frontend/proxy.ts
💤 Files with no reviewable changes (7)
  • frontend/parse/javascript-quiz-part2.json
  • frontend/parse/react-quiz-data-part2.json
  • frontend/parse/javascript-quiz-part4.json
  • frontend/parse/javascript-quiz-part3.json
  • frontend/db/schema/users.ts
  • frontend/parse/javascript-quiz-part1.json
  • frontend/parse/react-quiz-data-part1.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-30T16:01:02.523Z
Learnt from: kryvosheyin
Repo: DevLoversTeam/devlovers.net PR: 88
File: frontend/drizzle/schema.ts:153-153
Timestamp: 2025-12-30T16:01:02.523Z
Learning: The file frontend/drizzle/schema.ts is a legacy file not used by Drizzle or the runtime. The canonical schemas live in frontend/db/schema/* as configured in drizzle-config.ts, and that's what migrations and runtime use.

Applied to files:

  • frontend/drizzle/meta/0010_snapshot.json
  • frontend/drizzle/meta/0009_snapshot.json
  • frontend/db/schema/points.ts
  • frontend/db/seed-quiz-typescript-advanced.ts
  • frontend/db/seed-quiz-typescript.ts
  • frontend/db/seed-quiz-html-advanced.ts
  • frontend/db/seed-demo-leaderboard.ts
🧬 Code graph analysis (17)
frontend/app/[locale]/leaderboard/page.tsx (1)
frontend/app/api/questions/[category]/route.ts (1)
  • revalidate (6-6)
frontend/db/queries/leaderboard.ts (2)
frontend/db/schema/points.ts (1)
  • pointTransactions (13-27)
frontend/db/schema/users.ts (1)
  • users (11-42)
frontend/db/seed-quiz-typescript-advanced.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/queries/quiz.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (2)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/seed-quiz-css-advanced.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/seed-quiz-typescript.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/seed-quiz-git.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/seed-quiz-html.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/queries/points.ts (3)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (1)
  • quizAttempts (129-164)
frontend/db/schema/points.ts (1)
  • pointTransactions (13-27)
frontend/db/queries/users.ts (3)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/points.ts (1)
  • pointTransactions (13-27)
frontend/drizzle/schema.ts (1)
  • pointTransactions (128-142)
frontend/db/seed-quiz-css.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/db/seed-quiz-javascript-advanced.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/app/api/quiz/guest-result/route.ts (4)
frontend/lib/auth.ts (1)
  • getCurrentUser (94-115)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (4)
  • quizQuestions (59-79)
  • quizAnswers (96-111)
  • quizAttempts (129-164)
  • quizAttemptAnswers (166-192)
frontend/db/queries/points.ts (1)
  • calculateQuizPoints (7-12)
frontend/app/[locale]/quiz/[slug]/page.tsx (1)
frontend/db/queries/quiz.ts (1)
  • getQuizQuestionsRandomized (252-259)
frontend/db/seed-quiz-html-advanced.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/actions/quiz.ts (4)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (4)
  • quizQuestions (59-79)
  • quizAnswers (96-111)
  • quizAttempts (129-164)
  • quizAttemptAnswers (166-192)
frontend/lib/auth.ts (1)
  • getCurrentUser (94-115)
frontend/db/queries/points.ts (2)
  • calculateQuizPoints (7-12)
  • awardQuizPoints (38-74)
frontend/proxy.ts (1)
frontend/lib/auth.ts (1)
  • AuthTokenPayload (23-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - develop-devlovers
  • GitHub Check: Header rules - develop-devlovers
  • GitHub Check: Pages changed - develop-devlovers
🔇 Additional comments (46)
frontend/components/quiz/CountdownTimer.tsx (1)

23-33: Good improvement: Date.now()-based calculation with 100ms polling.

Using a fixed endTime with high-frequency polling is more accurate than decrementing state per second, especially when browsers throttle background tab timers. The queueMicrotask for onTimeUp correctly defers the callback to avoid state updates during render.

frontend/proxy.ts (1)

43-52: Token validation logic is correct, pending signature verification fix.

The token validation and expiration check logic is properly implemented. However, this function's security depends on fixing the signature verification issue in decodeAuthToken (see comment on lines 19-41).

frontend/app/api/auth/signup/route.ts (1)

21-80: Error handling implementation looks good.

The try/catch wrapper with error logging and detailed error responses provides good observability and debugging support. The 500 status code is appropriate for server errors.

frontend/parse/typescript/advanced/typescript-advanced-quiz-part3.json (1)

1-434: Well-structured quiz data with comprehensive multilingual support.

The JSON structure is consistent and complete with proper question metadata, multilingual prompts/explanations, and correctly flagged answers. The content covers advanced TypeScript topics appropriately for the difficulty level.

frontend/parse/typescript/fundamentals/typescript-quiz-part3.json (1)

1-434: Well-structured fundamentals quiz data with appropriate difficulty levels.

The JSON structure is consistent and complete with proper multilingual support. The questions cover essential TypeScript fundamentals (utility types, conditional types, decorators, modules, configuration) at beginner and medium difficulty levels.

frontend/parse/typescript/fundamentals/typescript-quiz-part4.json (1)

1-434: LGTM!

The quiz data structure is well-formed with consistent localization across uk/en/pl, proper difficulty assignments, and technically accurate TypeScript questions covering type narrowing, declaration files, configuration, OOP concepts, and utility types.

frontend/parse/typescript/advanced/typescript-advanced-quiz-part1.json (1)

1-434: LGTM!

Well-structured advanced TypeScript quiz covering decorators, reflection, type guards, and utility types. Localization is complete and technically accurate.

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

4-4: LGTM!

Increasing ISR revalidation from 300s to 3600s aligns with the query-level caching in getLeaderboardData. This is appropriate for leaderboard data that doesn't require real-time updates.

frontend/drizzle/meta/_journal.json (1)

68-81: LGTM!

The migration journal entries are correctly formatted and document the two new SQL migrations introduced in this PR.

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

23-24: Verify: Is Date.now() seed providing the intended "stable randomization"?

The PR objective mentions "Add seed to randomizeQuizQuestions for stable randomization," but using Date.now() as the seed generates a different value on every page load (changing every millisecond). This results in a different question order each time the page is accessed, which is the opposite of stable/deterministic randomization.

If the goal is to have consistent question order for a given user or session:

  • Consider using a hash of userId + quizId, or
  • Store the seed in session storage, or
  • Use a fixed seed per quiz instance

If the current behavior (different order every time) is intentional, the PR objective wording may need clarification.

Could you confirm whether you want:

  1. Different question order on each page load (current implementation), or
  2. Consistent question order per user/session (requires stable seed source)?
frontend/parse/typescript/advanced/typescript-advanced-quiz-part2.json (1)

1-434: LGTM!

The quiz data is well-structured with:

  • Accurate TypeScript technical content covering advanced topics (ReturnType, Parameters, infer, template literals, tsconfig options)
  • Consistent schema across all 10 questions
  • Complete translations for uk/en/pl locales
  • Exactly one correct answer per question

The content accurately represents TypeScript advanced concepts and compiler configuration.

frontend/drizzle/0010_parallel_princess_powerful.sql (3)

3-3: LGTM!

Adding points_earned column with default value 0 correctly implements the ledger-based points model described in the PR objectives.


4-4: LGTM!

Adding an index on point_transactions.user_id is essential for efficiently aggregating user points via SUM(point_transactions.points) in the new ledger-based model.


5-5: Verify: Application code updated to handle dropped users.points column.

This migration correctly implements the ledger-based points model by removing the denormalized points column. However, the dashboard code still references user.points (see frontend/app/[locale]/dashboard/page.tsx line 45), which will cause a runtime error after this migration runs.

Ensure that:

  1. getUserProfile query is updated to compute points from point_transactions
  2. All components consuming user points are updated
  3. The dashboard displays points from the aggregated ledger value

Related to the critical issue flagged in frontend/app/[locale]/dashboard/page.tsx.

frontend/parse/typescript/fundamentals/typescript-quiz-part2.json (1)

1-434: LGTM! Well-structured TypeScript fundamentals quiz data.

The quiz questions (ts-q11 to ts-q20) are technically accurate, cover appropriate TypeScript concepts (access modifiers, abstract classes, type operators, generics, tuples), and include complete translations for all three locales (uk, en, pl). The JSON structure is valid and consistent with the existing quiz data format.

frontend/db/queries/leaderboard.ts (2)

16-21: LGTM! Proper ledger-based aggregation with LEFT JOIN.

The query correctly:

  • Uses LEFT JOIN to include users with no point transactions
  • Applies COALESCE to default NULL sums to 0
  • Groups by all user fields to satisfy SQL requirements
  • Orders by the computed total

The migration from a denormalized users.points column to aggregating pointTransactions is implemented correctly.


44-44: Cache revalidation window increased significantly.

The revalidate time has been increased from 300 seconds (5 minutes) to 3600 seconds (1 hour), a 12× increase. This reduces database load but means leaderboard updates will be delayed by up to an hour.

Verify this aligns with product requirements for leaderboard freshness, especially after quiz completions.

frontend/db/schema/points.ts (1)

9-9: LGTM! Essential index for leaderboard performance.

Adding point_transactions_user_id_idx on userId is critical for efficient queries that:

  • Join pointTransactions with users (as in the leaderboard query)
  • Filter by userId when calculating user totals
  • Aggregate points per user

This index will significantly improve query performance as the point transactions table grows.

Also applies to: 25-27

frontend/db/seed-demo-leaderboard.ts (2)

57-77: Verify cache invalidation after seeding.

The seed flow correctly:

  1. Inserts demo users with onConflictDoNothing to avoid overwriting existing users
  2. Deletes previous seed point transactions (filtered by source='seed' and demo user IDs)
  3. Inserts fresh seed point transactions with proper metadata

However, since the leaderboard query uses unstable_cache with the 'leaderboard' tag, consider adding cache invalidation after seeding to ensure the leaderboard reflects the new seed data immediately.

💡 Suggested cache invalidation

After line 77, add:

import { revalidateTag } from 'next/cache';

// After inserting point transactions
await db.insert(pointTransactions).values(
  demoUsers.map(u => ({
    userId: u.id,
    points: u.points,
    source: 'seed',
    metadata: { reason: 'demo' },
  }))
);

// Invalidate leaderboard cache
revalidateTag('leaderboard');

Note: revalidateTag is typically used in server actions/route handlers, not seed scripts. For seed scripts, the cache will naturally expire, or you may need to restart the dev server.


45-77: LGTM! Clean seed flow with proper transaction ledger.

The updated seeding logic correctly migrates from direct user points to the transaction-based ledger model:

  • Uses onConflictDoNothing to safely handle re-runs
  • Cleans up previous seed transactions by filtering on source='seed'
  • Creates new point transactions with proper source tracking and metadata

The demo user points field (lines 15, 22, 29, 35, 41) is appropriately used only for seeding the ledger at line 73.

frontend/drizzle/meta/0010_snapshot.json (1)

1-1954: LGTM — Auto-generated migration snapshot.

This Drizzle snapshot correctly reflects the schema changes:

  • points_earned column added to quiz_attempts (lines 546-552)
  • point_transactions_user_id_idx index present (lines 1145-1159)
  • users.points column removed as expected

The foreign key constraints and cascade behaviors are properly configured.

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

164-186: Excellent optimization — eliminates N+1 queries.

The two-query approach with batch loading is a significant improvement. Early return on empty questions prevents unnecessary work.

frontend/db/queries/points.ts (2)

28-35: Good optimization using MAX aggregate.

Using a single aggregate query instead of fetching all attempts and computing max in JavaScript is more efficient.


55-73: No issue — revalidateTag correctly accepts the second argument.

revalidateTag in newer versions of Next.js accepts a second parameter for the cache-life profile. The call revalidateTag('leaderboard', 'default') is valid—'default' is a built-in named profile (stale: 5 minutes, revalidate: 15 minutes, expire: 1 year). No changes needed.

Likely an incorrect or invalid review comment.

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

27-39: Clean implementation of ledger-based points calculation.

Good approach: fetch user first, then aggregate points from the ledger. The early return for missing user prevents unnecessary work.

Minor observation: sql<number> already types the result, so Number(pointsResult[0]?.total) || 0 is slightly redundant with the COALESCE, but provides defense-in-depth against edge cases.

frontend/parse/typescript/fundamentals/typescript-quiz-part1.json (1)

1-434: Well-structured quiz content.

The quiz data is well-organized with:

  • Consistent structure across all 10 questions
  • Complete trilingual support (uk, en, pl)
  • Exactly one correct answer per question
  • Accurate TypeScript fundamentals content
frontend/parse/typescript/advanced/typescript-advanced-quiz-part4.json (1)

1-434: Advanced TypeScript quiz content looks good.

The quiz questions cover advanced TypeScript topics appropriately. Structure is consistent with other quiz files.

Note: Some questions (e.g., q33, q34, q35) are marked as "medium" difficulty within the "advanced" quiz. This may be intentional for difficulty progression, but verify this aligns with how difficulty is used elsewhere in the quiz system.

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

22-40: Clean state extension for points tracking.

The pointsAwarded state field and optional COMPLETE_QUIZ payload are well-designed. The nullable type correctly represents the three states: not yet completed (null), completed with points (number), or completed with error/guest (null via fallback).


189-210: Good handling of points in submission flow.

The differentiation between success (dispatches with pointsAwarded: 0 or actual value) and error (dispatches without payload, resulting in null) allows QuizResult to distinguish these cases for appropriate UI feedback.


291-309: Props correctly passed to QuizResult.

The answeredCount and pointsAwarded props enable the result component to display appropriate feedback about points earned vs. no improvement.

frontend/drizzle/0009_unknown_nico_minoru.sql (1)

1-18: Indexes and constraints look well-designed for query optimization and data integrity.

The new indexes appropriately target common query patterns (quiz attempts by user/quiz, questions by category/sort order, etc.). The CHECK constraints enforce important business invariants like price consistency between minor and major currency units.

One consideration: Line 16 hardcodes currency = 'USD'. If multi-currency support is planned in the future, this constraint will need migration.

frontend/db/seed-quiz-typescript-advanced.ts (1)

1-238: Same structural patterns and duplication as other seed scripts.

This script follows the same pattern as the other quiz seed scripts. The refactoring suggestion from the CSS advanced review applies here as well.

frontend/db/seed-quiz-typescript.ts (1)

1-240: Follows the established seed script pattern.

This script is consistent with the other seed scripts in the PR. The same refactoring opportunity exists to extract shared logic.

frontend/actions/quiz.ts (4)

193-202: Authentication and authorization checks are well-implemented.

The session validation and user ID mismatch check properly protect against unauthorized access and prevent users from submitting attempts on behalf of others.


204-209: Empty answers array rejected; verify this aligns with intended behavior.

The PR summary mentions "handle empty answers arrays" but this code returns an error when answers.length === 0. If the intent is to allow submitting with zero answers (e.g., time expired), this check should be adjusted.


211-267: Robust validation of questions and answers.

The batch retrieval of question IDs and answer records eliminates N+1 queries. The validation logic properly:

  • Verifies questions belong to the quiz
  • Detects duplicate answers
  • Ensures answers match their questions

This addresses security concerns around answer manipulation.


298-338: Points calculation and awarding logic is correct.

The dual-step approach is intentional:

  1. calculateQuizPoints computes the full score for pointsEarned storage in the attempt record
  2. awardQuizPoints calculates delta versus previous best and creates a ledger transaction

This supports the ledger-based points model described in the PR objectives.

frontend/db/seed-quiz-git.ts (4)

1-37: LGTM!

The imports and type definitions are well-structured and appropriate for the seed script.


38-62: LGTM!

The quiz metadata and explanation helper are well-defined with proper multi-locale support.


70-137: LGTM!

The ensureQuizExists function has proper safeguards against data loss when quiz attempts exist, and handles both create and update scenarios correctly.


139-178: LGTM!

The question seeding logic correctly handles questions, localized content, answers, and answer translations. While individual inserts could be batched for performance, this is acceptable for an infrequently-run seed script.

frontend/app/api/quiz/guest-result/route.ts (5)

102-127: LGTM!

The input validation and authentication check are properly implemented, preventing invalid payloads and unauthorized access.


129-146: LGTM!

The quiz existence check and answer count validation ensure data integrity.


148-176: LGTM!

The answer validation loop efficiently detects invalid question IDs, missing fields, and duplicate answers using Set-based lookups.


178-219: LGTM!

The batch answer lookup and cross-validation between selected answers and their associated questions is well-implemented, ensuring data integrity before persistence.


221-262: LGTM!

The points logic correctly distinguishes between:

  • pointsEarned: the full score stored on this attempt
  • pointsAwarded: the delta awarded vs previous best (via awardQuizPoints)

This aligns with the PR objectives for the ledger-based points system.

email: user.email ?? '',
role: user.role ?? null,
points: realPoints,
points: user.points ?? 0,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Referencing dropped column user.points.

Migration 0010_parallel_princess_powerful.sql drops the points column from the users table (line 5), but this code still references user.points. This will cause a runtime error once the migration is applied.

According to the PR objectives, points should now be computed from SUM(point_transactions.points). The getUserProfile query needs to be updated to aggregate points from the point_transactions table using a JOIN and SUM, or points should be fetched separately and passed to this component.

🔎 Verify the issue

Run the following script to check how getUserProfile currently fetches user data:

#!/bin/bash
# Description: Check getUserProfile implementation to see if it computes points from transactions

# Search for getUserProfile function definition
ast-grep --pattern $'export async function getUserProfile($$$) {
  $$$
}'

# Also search for point_transactions references in queries/users.ts
rg -nP --type=ts -C5 'getUserProfile|point_transactions' -g '**/queries/users.ts'
🤖 Prompt for AI Agents
In frontend/app/[locale]/dashboard/page.tsx around line 45, the code still reads
user.points but the migration removed that column; update data flow so points
are computed from point_transactions (SUM of point_transactions.points) and
returned as a numeric field (e.g. points or pointsTotal) by getUserProfile, or
fetch points in a separate query and pass that value into this component; change
the component to use the new returned field (or the separately fetched variable)
instead of user.points and ensure types/null handling use 0 as a fallback.

Comment on lines +17 to 18
const [endTime] = useState(() => Date.now() + timeLimitSeconds * 1000);
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

endTime won't update if timeLimitSeconds prop changes.

The useState initializer only runs once. If this component is reused without unmounting (e.g., navigating between quizzes), the endTime will remain stale while timeLimitSeconds changes, causing incorrect countdown and percentage calculation (line 55 uses timeLimitSeconds directly).

Either ensure the parent passes a key prop to force remount on quiz change, or recalculate endTime when timeLimitSeconds changes:

Option: Reset endTime on timeLimitSeconds change
- const [endTime] = useState(() => Date.now() + timeLimitSeconds * 1000);
+ const [endTime, setEndTime] = useState(() => Date.now() + timeLimitSeconds * 1000);
  const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);

+ useEffect(() => {
+   setEndTime(Date.now() + timeLimitSeconds * 1000);
+   setRemainingSeconds(timeLimitSeconds);
+ }, [timeLimitSeconds]);
📝 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
const [endTime] = useState(() => Date.now() + timeLimitSeconds * 1000);
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
const [endTime, setEndTime] = useState(() => Date.now() + timeLimitSeconds * 1000);
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
useEffect(() => {
setEndTime(Date.now() + timeLimitSeconds * 1000);
setRemainingSeconds(timeLimitSeconds);
}, [timeLimitSeconds]);
🤖 Prompt for AI Agents
frontend/components/quiz/CountdownTimer.tsx around lines 17 to 18: endTime is
initialized once with useState(() => Date.now() + timeLimitSeconds * 1000) so it
will stay stale if the timeLimitSeconds prop changes; update the component to
recalculate endTime whenever timeLimitSeconds changes (and reset
remainingSeconds) instead of relying on a one-time initializer — e.g., store
endTime in state and add a useEffect that sets endTime = Date.now() +
timeLimitSeconds * 1000 and setRemainingSeconds(timeLimitSeconds) when
timeLimitSeconds changes; alternatively, document that callers must force a
remount by passing a key when timeLimitSeconds changes.

Comment on lines 186 to 190
if (!partArg) {
console.error('Error: Please specify which part to upload');
console.log('Usage: npx tsx db/seeds/seed-quiz-javascript.ts <part-number>');
console.log('Example: npx tsx db/seeds/seed-quiz-javascript.ts 1');
console.log('Or upload all: npx tsx db/seeds/seed-quiz-javascript.ts all');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CLI usage message shows incorrect file path.

The usage message references db/seeds/seed-quiz-javascript.ts but the actual file is seed-quiz-javascript-advanced.ts. This could confuse users.

🔎 Proposed fix
-    console.log('Usage: npx tsx db/seeds/seed-quiz-javascript.ts <part-number>');
-    console.log('Example: npx tsx db/seeds/seed-quiz-javascript.ts 1');
-    console.log('Or upload all: npx tsx db/seeds/seed-quiz-javascript.ts all');
+    console.log('Usage: npx tsx db/seed-quiz-javascript-advanced.ts <part-number>');
+    console.log('Example: npx tsx db/seed-quiz-javascript-advanced.ts 1');
+    console.log('Or upload all: npx tsx db/seed-quiz-javascript-advanced.ts all');
📝 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
if (!partArg) {
console.error('Error: Please specify which part to upload');
console.log('Usage: npx tsx db/seeds/seed-quiz-javascript.ts <part-number>');
console.log('Example: npx tsx db/seeds/seed-quiz-javascript.ts 1');
console.log('Or upload all: npx tsx db/seeds/seed-quiz-javascript.ts all');
if (!partArg) {
console.error('Error: Please specify which part to upload');
console.log('Usage: npx tsx db/seed-quiz-javascript-advanced.ts <part-number>');
console.log('Example: npx tsx db/seed-quiz-javascript-advanced.ts 1');
console.log('Or upload all: npx tsx db/seed-quiz-javascript-advanced.ts all');
🤖 Prompt for AI Agents
In frontend/db/seed-quiz-javascript-advanced.ts around lines 186 to 190, the CLI
usage and example messages reference the wrong filename
(db/seeds/seed-quiz-javascript.ts); update those strings to reference the actual
file path/name (seed-quiz-javascript-advanced.ts) so the usage, example and
"upload all" guidance reflect the correct script path.

Comment on lines +1 to +2
DROP INDEX "quiz_attempts_user_id_idx";--> statement-breakpoint
DROP INDEX "quiz_attempts_quiz_id_idx";--> statement-breakpoint
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major performance regression: Dropping essential indices.

Removing quiz_attempts_user_id_idx and quiz_attempts_quiz_id_idx will severely degrade query performance for common operations:

  1. getUserQuizStats(userId) - requires full table scan without user_id index
  2. Finding previous best attempts - needs user_id + quiz_id for the "MAX(pointsEarned)" optimization mentioned in PR objectives
  3. Leaderboard queries - likely join on quiz_attempts by user_id
  4. Quiz-specific attempt lookups - need quiz_id index

The PR states "Optimize previous-best lookup using MAX(pointsEarned)" but removing these indices contradicts that goal.

🔎 Verify query patterns requiring these indices
#!/bin/bash
# Description: Find queries that filter by user_id or quiz_id on quiz_attempts

# Search for queries filtering quiz_attempts by userId
rg -nP --type=ts -C3 'quiz_?attempts.*userId|where.*userId.*quiz_?attempts' -g '**/queries/**'

# Search for queries filtering by quizId
rg -nP --type=ts -C3 'quiz_?attempts.*quizId|where.*quizId.*quiz_?attempts' -g '**/queries/**'

# Check getUserQuizStats implementation
ast-grep --pattern $'export async function getUserQuizStats($$$) {
  $$$
}'

Recommendation: Keep or re-add these indices. Consider a composite index on (user_id, quiz_id, points_earned) for the best-attempt lookup optimization.

🤖 Prompt for AI Agents
In frontend/drizzle/0010_parallel_princess_powerful.sql around lines 1-2, the
migration drops two critical indices (quiz_attempts_user_id_idx and
quiz_attempts_quiz_id_idx) which will cause major query regressions; restore
these indices by removing the DROP INDEX lines from this migration, or replace
them with CREATE INDEX statements that re-add the single-column indexes, and for
the "previous-best" optimization add a composite index on (user_id, quiz_id,
points_earned) (or at minimum a composite (user_id, quiz_id) plus an index on
points_earned) so queries like getUserQuizStats, previous-best lookups,
leaderboard joins, and quiz-specific lookups retain efficient plans.

Comment on lines +19 to +41
function decodeAuthToken(token: string): AuthTokenPayload | null {
const parts = token.split(".");
if (parts.length < 2) return null;

const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");

try {
const json = atob(padded);
const payload = JSON.parse(json) as Partial<AuthTokenPayload>;
if (
typeof payload.userId !== "string" ||
(payload.role !== "user" && payload.role !== "admin") ||
typeof payload.email !== "string" ||
typeof payload.exp !== "number"
) {
return null;
}
return payload as AuthTokenPayload;
} catch {
return null;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: JWT signature verification is missing.

The decodeAuthToken function manually parses the JWT payload without verifying the cryptographic signature. This creates a critical security vulnerability where an attacker could forge a token with arbitrary userId, role, email, and exp values. The forged token would pass validation as long as it's properly formatted JSON.

🔎 Use a proper JWT library with signature verification
+import { jwtVerify } from 'jose';
+
-function decodeAuthToken(token: string): AuthTokenPayload | null {
-  const parts = token.split(".");
-  if (parts.length < 2) return null;
-
-  const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
-  const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
-
-  try {
-    const json = atob(padded);
-    const payload = JSON.parse(json) as Partial<AuthTokenPayload>;
-    if (
-      typeof payload.userId !== "string" ||
-      (payload.role !== "user" && payload.role !== "admin") ||
-      typeof payload.email !== "string" ||
-      typeof payload.exp !== "number"
-    ) {
-      return null;
-    }
-    return payload as AuthTokenPayload;
-  } catch {
-    return null;
-  }
+async function decodeAuthToken(token: string): Promise<AuthTokenPayload | null> {
+  try {
+    const secret = new TextEncoder().encode(AUTH_SECRET);
+    const { payload } = await jwtVerify(token, secret);
+    
+    if (
+      typeof payload.userId !== "string" ||
+      (payload.role !== "user" && payload.role !== "admin") ||
+      typeof payload.email !== "string" ||
+      typeof payload.exp !== "number"
+    ) {
+      return null;
+    }
+    return payload as AuthTokenPayload;
+  } catch {
+    return null;
+  }
 }

Note: This requires updating isAuthenticated to be async and awaiting the result.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/proxy.ts around lines 19 to 41 the function decodeAuthToken only
base64-decodes and parses the JWT payload without cryptographic signature
verification; replace this manual parsing with a proper JWT verification step
(e.g., use jsonwebtoken/jose) that verifies the token signature with the
configured secret or public key and allowed algorithms, check the exp claim
after verification, throw/return null on verification failure, and update any
callers (like isAuthenticated) to be async and await the verification result;
also remove use of atob and any manual base64 padding logic and ensure
verification errors are handled and logged appropriately.

@LesiaUKR LesiaUKR closed this Jan 3, 2026
Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
frontend/lib/auth.ts (1)

42-60: Add validation for the exp field in the type guard.

The isAuthTokenPayload type guard validates userId, role, and email but doesn't check the newly added exp field. This creates a type safety gap where tokens without or with invalid exp values could incorrectly pass validation.

🔎 Proposed fix to validate the exp field
 function isAuthTokenPayload(value: unknown): value is AuthTokenPayload {
   if (typeof value !== "object" || value === null) return false;
 
-  if (!("userId" in value) || !("role" in value) || !("email" in value)) {
+  if (!("userId" in value) || !("role" in value) || !("email" in value) || !("exp" in value)) {
     return false;
   }
 
   const v = value as {
     userId: unknown;
     role: unknown;
     email: unknown;
+    exp: unknown;
   };
 
   return (
     typeof v.userId === "string" &&
     typeof v.email === "string" &&
-    (v.role === "user" || v.role === "admin")
+    (v.role === "user" || v.role === "admin") &&
+    typeof v.exp === "number"
   );
 }
♻️ Duplicate comments (6)
frontend/proxy.ts (1)

13-35: Critical: JWT signature verification is still missing.

As previously flagged, the decodeAuthToken function only decodes and parses the JWT payload without verifying the cryptographic signature. An attacker can forge tokens with arbitrary userId, role, email, and exp values that will be accepted as valid.

Additionally, this duplicates logic already present in verifyAuthToken (lines 62-74 in frontend/lib/auth.ts), which properly uses jwt.verify for signature verification.

🔎 Proposed fix using the existing verifyAuthToken function

Since verifyAuthToken already exists in frontend/lib/auth.ts, import and use it instead:

-import { AuthTokenPayload } from '@/lib/auth';
+import { AuthTokenPayload, verifyAuthToken } from '@/lib/auth';

 const AUTH_COOKIE_NAME = 'auth_session';

-const AUTH_SECRET = process.env.AUTH_SECRET;
-if (!AUTH_SECRET) {
-  throw new Error('AUTH_SECRET is not defined');
-}
-
-function decodeAuthToken(token: string): AuthTokenPayload | null {
-  const parts = token.split(".");
-  if (parts.length < 2) return null;
-
-  const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
-  const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
-
-  try {
-    const json = atob(padded);
-    const payload = JSON.parse(json) as Partial<AuthTokenPayload>;
-    if (
-      typeof payload.userId !== "string" ||
-      (payload.role !== "user" && payload.role !== "admin") ||
-      typeof payload.email !== "string" ||
-      typeof payload.exp !== "number"
-    ) {
-      return null;
-    }
-    return payload as AuthTokenPayload;
-  } catch {
-    return null;
-  }
-}
-
 function isAuthenticated(req: NextRequest): boolean {
   const token = req.cookies.get(AUTH_COOKIE_NAME)?.value;
   if (!token) return false;
 
-  const payload = decodeAuthToken(token);
+  const payload = verifyAuthToken(token);
   if (!payload) return false;
 
   const now = Math.floor(Date.now() / 1000);
   return payload.exp > now;
 }
frontend/app/[locale]/dashboard/page.tsx (1)

45-45: Verify that getUserProfile returns computed points from ledger.

The past review comment correctly flagged that the points column was dropped from the users table. According to the enriched summary, getUserProfile should now compute points from point_transactions. This line will work correctly if getUserProfile properly aggregates and returns a points field.

Run the following script to verify that getUserProfile computes points from the point_transactions table:

#!/bin/bash
# Description: Verify getUserProfile implementation computes points from point_transactions

# Check getUserProfile implementation
ast-grep --pattern $'export async function getUserProfile($$$) {
  $$$
}'

# Search for point_transactions references in the queries/users file
rg -nP --type=ts -C10 'getUserProfile' -g '**/queries/users.ts'

# Verify point_transactions aggregation logic
rg -nP --type=ts -C5 'SUM.*point_transactions|point_transactions.*SUM' -g '**/queries/**'
frontend/db/seed-quiz-javascript-advanced.ts (2)

1-1: BOM character detected.

This issue was already flagged in a previous review.


186-191: CLI usage message shows incorrect file path.

This issue was already flagged in a previous review.

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

142-146: Debug console.log statement.

This issue was already flagged in a previous review.

frontend/db/seed-quiz-css.ts (1)

186-191: Usage instructions reference incorrect path.

This issue was already flagged in a previous review.

🧹 Nitpick comments (10)
frontend/db/seed-quiz-javascript-advanced.ts (1)

212-212: Hardcoded answer count assumption.

The calculation assumes exactly 4 answers per question (* 4). If the question format changes to support variable answer counts, this will be incorrect. Consider computing the actual total from the loaded data.

🔎 Alternative approach

Track the actual answer count while seeding:

       for (let i = 1; i <= 4; i++) {
         const questions = await loadQuestions(i);
         await seedQuestions(questions, quizId, i);
         totalQuestions += questions.length;
+        totalAnswers += questions.reduce((sum, q) => sum + q.answers.length, 0);
       }

       console.log('\nAll parts seeded successfully!');
       console.log(`   - 1 quiz with ${LOCALES.length} translations`);
       console.log(`   - ${totalQuestions} questions total`);
-      console.log(`   - ${totalQuestions * 4} answers with ${LOCALES.length} translations each`);
+      console.log(`   - ${totalAnswers} answers with ${LOCALES.length} translations each`);

Apply the same pattern at line 227.

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

116-132: Consider extracting nested ternaries for clarity.

The nested ternary operators for styling and content could be extracted into computed values or a helper function to improve readability.

🔎 Possible refactor
+      const pointsStyles = pointsAwarded > 0
+        ? {
+            container: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
+            text: 'text-green-800 dark:text-green-200',
+            message: `+${pointsAwarded} балів додано до рейтингу`
+          }
+        : {
+            container: 'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800',
+            text: 'text-gray-600 dark:text-gray-400',
+            message: 'Бали не нараховано (результат не покращено)'
+          };
+
       {!isGuest && pointsAwarded !== null && pointsAwarded !== undefined && (
-        <div className={`p-4 rounded-xl border ${
-          pointsAwarded > 0 
-            ? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' 
-            : 'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800'
-        }`}>
-          <p className={`text-center font-medium ${
-            pointsAwarded > 0 
-              ? 'text-green-800 dark:text-green-200' 
-              : 'text-gray-600 dark:text-gray-400'
-          }`}>
-            {pointsAwarded > 0 
-              ? `+${pointsAwarded} балів додано до рейтингу` 
-              : 'Бали не нараховано (результат не покращено)'}
+        <div className={`p-4 rounded-xl border ${pointsStyles.container}`}>
+          <p className={`text-center font-medium ${pointsStyles.text}`}>
+            {pointsStyles.message}
           </p>
         </div>
       )}
frontend/db/seed-quiz-css.ts (1)

212-212: Hardcoded answer count assumption.

The calculation assumes exactly 4 answers per question (* 4). Consider computing the actual total from the loaded data for better maintainability.

🔎 Alternative approach

Track the actual answer count while seeding (same pattern as suggested for the JavaScript seed):

       for (let i = 1; i <= 4; i++) {
         const questions = await loadQuestions(i);
         await seedQuestions(questions, quizId, i);
         totalQuestions += questions.length;
+        totalAnswers += questions.reduce((sum, q) => sum + q.answers.length, 0);
       }

       console.log('\nAll parts seeded successfully!');
       console.log(`   - 1 quiz with ${LOCALES.length} translations`);
       console.log(`   - ${totalQuestions} questions total`);
-      console.log(`   - ${totalQuestions * 4} answers with ${LOCALES.length} translations each`);
+      console.log(`   - ${totalAnswers} answers with ${LOCALES.length} translations each`);

Apply the same pattern at line 227.

json/quizzes/css/advanced/seed-quiz-css-advanced.ts (2)

199-207: Running "all" multiple times may create duplicate questions.

When ensureQuizExists() is called, it deletes existing questions for the quiz. However, when seeding individual parts (e.g., seedQuizFromJson 1 then seedQuizFromJson 2), the second run calls ensureQuizExists() again which clears all questions including part 1. This design means:

  • Running all works correctly (clears then seeds all)
  • Running individual parts sequentially will lose previously seeded parts

This may be intentional, but worth documenting in the usage instructions that individual part seeding replaces all content.


64-68: Consider adding file existence check before reading.

readFileSync will throw if the file doesn't exist. Adding a check would provide a clearer error message.

🔎 Proposed improvement
+import { existsSync, readFileSync } from 'fs';

 async function loadQuestions(partNumber: number): Promise<QuestionData[]> {
   const partPath = join(process.cwd(), 'data', `css-advanced-quiz-part${partNumber}.json`);
+  if (!existsSync(partPath)) {
+    throw new Error(`Quiz data file not found: ${partPath}`);
+  }
   const partData: QuizPartData = JSON.parse(readFileSync(partPath, 'utf-8'));
   return partData.questions;
 }
json/quizzes/css/fundamentals/seed-quiz-css.ts (1)

1-240: Significant code duplication with seed-quiz-css-advanced.ts.

This file shares ~95% of its code with seed-quiz-css-advanced.ts. Consider extracting shared logic into a reusable seed utility to reduce duplication and maintenance burden.

The following could be extracted into a shared module:

  • Locale type and LOCALES constant
  • QuestionData and QuizPartData interfaces
  • createExplanation function
  • loadQuestions function (parameterized by path pattern)
  • ensureQuizExists function (parameterized by metadata)
  • seedQuestions function
  • CLI parsing logic in seedQuizFromJson
🔎 Example shared utility structure
// db/seed-utils/quiz-seeder.ts
export interface QuizSeedConfig {
  categorySlug: string;
  quizSlug: string;
  displayOrder: number;
  questionsCount: number;
  timeLimitSeconds: number;
  translations: Record<Locale, { title: string; description: string }>;
  dataPathPattern: string; // e.g., 'css-quiz-part{n}.json'
  maxParts: number;
}

export async function seedQuiz(config: QuizSeedConfig) {
  // Shared implementation
}

Then each seed script becomes:

// seed-quiz-css.ts
import { seedQuiz } from './seed-utils/quiz-seeder';
seedQuiz({ categorySlug: 'css', quizSlug: 'css-fundamentals', ... });
frontend/actions/quiz.ts (2)

10-10: Minor formatting: add space after comma in import.

The import statement is missing a space after the comma between awardQuizPoints and calculateQuizPoints.

🔎 Proposed fix
-import { awardQuizPoints,calculateQuizPoints  } from '@/db/queries/points';
+import { awardQuizPoints, calculateQuizPoints } from '@/db/queries/points';

154-157: Consider rejecting invalid timestamps instead of silent fallback.

The code falls back to new Date() when answer.answeredAt is invalid. While defensive, this could mask data quality issues and create misleading timestamps in the audit trail.

Consider returning an error instead:

🔎 Alternative approach
     const answeredAt = new Date(answer.answeredAt);
-    const safeAnsweredAt = Number.isNaN(answeredAt.getTime())
-      ? new Date()
-      : answeredAt;
+    if (Number.isNaN(answeredAt.getTime())) {
+      return { success: false, error: 'Invalid timestamp in answer' };
+    }

     answerResults.push({
       questionId: answer.questionId,
       selectedAnswerId: answer.selectedAnswerId,
       isCorrect,
-      answeredAt: safeAnsweredAt,
+      answeredAt,
     });
frontend/app/api/quiz/guest-result/route.ts (2)

37-100: LGTM: Comprehensive validation matches quiz.ts pattern.

The validation flow correctly implements the same thorough checks as frontend/actions/quiz.ts:

  • Quiz existence and question count matching
  • Answer payload structure validation
  • Question validity and duplicate detection
  • Batch answer loading to avoid N+1 queries

Optional: Consider extracting the shared validation logic between this file and frontend/actions/quiz.ts into a shared utility function to reduce duplication and ensure consistency.


120-127: Consider building attemptAnswers after getting attemptId.

The placeholder attemptId: "" (line 121) is later overwritten via spread operator (lines 158-161). While functional, this pattern is less clear than building the array after obtaining the actual attempt ID.

🔎 Alternative approach
   const attemptAnswers = [];

   for (const answer of answers) {
     const record = answerById.get(answer.selectedAnswerId);
     if (!record || record.quizQuestionId !== answer.questionId) {
       return NextResponse.json(
         { success: false, error: "Answer does not match question" },
         { status: 400 }
       );
     }

     const isCorrect = record.isCorrect;
     if (isCorrect) correctAnswersCount++;

     attemptAnswers.push({
-      attemptId: "",
       quizQuestionId: answer.questionId,
       selectedAnswerId: answer.selectedAnswerId,
       isCorrect,
       answeredAt: now,
     });
   }
   
   // ... later ...
   
   await db.insert(quizAttemptAnswers).values(
-    attemptAnswers.map(a => ({
-      ...a,
+    attemptAnswers.map(a => ({
+      ...a,
       attemptId: attempt.id,
     }))
   );

Actually, on second thought, the current implementation is fine since the attemptId must be added after the insert. The spread operator pattern is appropriate here.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 863a080 and cb93738.

📒 Files selected for processing (43)
  • frontend/actions/quiz.ts
  • frontend/app/[locale]/dashboard/page.tsx
  • frontend/app/api/auth/signup/route.ts
  • frontend/app/api/quiz/guest-result/route.ts
  • frontend/components/quiz/QuizResult.tsx
  • frontend/db/seed-quiz-css.ts
  • frontend/db/seed-quiz-html-advanced.ts
  • frontend/db/seed-quiz-html.ts
  • frontend/db/seed-quiz-javascript-advanced.ts
  • frontend/lib/auth.ts
  • frontend/proxy.ts
  • json/quizzes/css/advanced/css-advanced-quiz-part1.json
  • json/quizzes/css/advanced/css-advanced-quiz-part2.json
  • json/quizzes/css/advanced/css-advanced-quiz-part3.json
  • json/quizzes/css/advanced/css-advanced-quiz-part4.json
  • json/quizzes/css/advanced/seed-quiz-css-advanced.ts
  • json/quizzes/css/fundamentals/css-quiz-part1.json
  • json/quizzes/css/fundamentals/css-quiz-part2.json
  • json/quizzes/css/fundamentals/css-quiz-part3.json
  • json/quizzes/css/fundamentals/css-quiz-part4.json
  • json/quizzes/css/fundamentals/seed-quiz-css.ts
  • json/quizzes/git/git-quiz-part1.json
  • json/quizzes/git/git-quiz-part2.json
  • json/quizzes/git/git-quiz-part3.json
  • json/quizzes/git/git-quiz-part4.json
  • json/quizzes/html/advanced/html-advanced-quiz-part1.json
  • json/quizzes/html/advanced/html-advanced-quiz-part2.json
  • json/quizzes/html/advanced/html-advanced-quiz-part3.json
  • json/quizzes/html/advanced/html-advanced-quiz-part4.json
  • json/quizzes/html/fundamentals/html-quiz-part1.json
  • json/quizzes/html/fundamentals/html-quiz-part2.json
  • json/quizzes/html/fundamentals/html-quiz-part3.json
  • json/quizzes/html/fundamentals/html-quiz-part4.json
  • json/quizzes/js/advanced/javascript-advanced-quiz-part1.json
  • json/quizzes/js/advanced/javascript-advanced-quiz-part2.json
  • json/quizzes/js/advanced/javascript-advanced-quiz-part3.json
  • json/quizzes/js/advanced/javascript-advanced-quiz-part4.json
  • json/quizzes/js/fundamentals/javascript-quiz-part1.json
  • json/quizzes/js/fundamentals/javascript-quiz-part2.json
  • json/quizzes/js/fundamentals/javascript-quiz-part3.json
  • json/quizzes/js/fundamentals/javascript-quiz-part4.json
  • json/quizzes/react/react-quiz-data-part1.json
  • json/quizzes/react/react-quiz-data-part2.json
✅ Files skipped from review due to trivial changes (4)
  • json/quizzes/git/git-quiz-part3.json
  • json/quizzes/html/fundamentals/html-quiz-part4.json
  • json/quizzes/css/fundamentals/css-quiz-part1.json
  • json/quizzes/git/git-quiz-part2.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/app/api/auth/signup/route.ts
  • frontend/db/seed-quiz-html.ts
  • frontend/db/seed-quiz-html-advanced.ts
🧰 Additional context used
🧬 Code graph analysis (6)
frontend/proxy.ts (1)
frontend/lib/auth.ts (1)
  • AuthTokenPayload (23-28)
frontend/db/seed-quiz-javascript-advanced.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
frontend/app/api/quiz/guest-result/route.ts (4)
frontend/lib/auth.ts (1)
  • getCurrentUser (95-116)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (4)
  • quizQuestions (59-79)
  • quizAnswers (96-111)
  • quizAttempts (129-164)
  • quizAttemptAnswers (166-192)
frontend/db/queries/points.ts (1)
  • calculateQuizPoints (7-12)
json/quizzes/css/advanced/seed-quiz-css-advanced.ts (1)
frontend/db/index.ts (1)
  • db (39-39)
json/quizzes/css/fundamentals/seed-quiz-css.ts (1)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/seed-quiz-css.ts (2)
frontend/db/index.ts (1)
  • db (39-39)
frontend/db/schema/quiz.ts (7)
  • quizzes (19-42)
  • quizAttempts (129-164)
  • quizQuestions (59-79)
  • quizTranslations (44-57)
  • quizQuestionContent (81-94)
  • quizAnswers (96-111)
  • quizAnswerTranslations (113-127)
🔇 Additional comments (34)
frontend/lib/auth.ts (1)

27-27: Good addition of the exp field.

This change addresses the duplicate type definition issue from the previous review and aligns with standard JWT payload structure.

frontend/proxy.ts (2)

4-4: Good refactor to import the shared type.

This addresses the past review comment about duplicate type definitions and follows the DRY principle.


37-46: Expiration validation is correct but depends on insecure token parsing.

The expiration check logic (lines 44-45) correctly validates that the token hasn't expired. However, this function depends on the insecure decodeAuthToken that lacks signature verification. Once decodeAuthToken is replaced with verifyAuthToken (as suggested above), the overall authentication flow will be secure.

json/quizzes/html/fundamentals/html-quiz-part2.json (1)

1-434: Excellent quiz data structure and content!

The HTML fundamentals quiz data is well-structured with:

  • Consistent JSON schema across all 10 questions
  • Proper multilingual support (Ukrainian, English, Polish)
  • Each question has exactly one correct answer
  • Appropriate difficulty levels and clear explanations
  • Factually accurate HTML content

The questions cover essential HTML topics (global attributes, hyperlinks, images, URLs) and the answer options are well-designed with plausible distractors.

json/quizzes/html/fundamentals/html-quiz-part1.json (1)

1-434: Well-crafted foundational quiz content!

The first part of the HTML fundamentals quiz is well-executed with:

  • Clear and accurate explanations of core HTML concepts
  • Consistent multilingual implementation
  • Properly marked correct answers
  • Appropriate beginner-level difficulty for introductory topics

The questions progress logically from HTML basics (abbreviation, role) through document structure (DOCTYPE, head/body) to element types (void elements, block vs inline, semantic markup).

json/quizzes/html/fundamentals/html-quiz-part3.json (1)

1-434: Comprehensive quiz content covering forms and tables!

The third part of the HTML fundamentals quiz maintains the high quality of the previous parts:

  • Well-structured questions on lists, forms, and tables
  • Accurate technical details (GET vs POST, HTML5 input types, semantic table elements)
  • Appropriate difficulty progression (mix of beginner and medium)
  • Consistent multilingual translations

The coverage of form elements (,

, , ) and table semantics (, , ) is particularly thorough and educationally sound.

json/quizzes/css/advanced/css-advanced-quiz-part1.json (1)

1-234: LGTM! Well-structured multilingual quiz data.

The JSON structure is consistent across all 10 questions, with proper multilingual support (uk, en, pl) for questions, explanations, and answers. The CSS content appears technically accurate and covers advanced topics appropriately.

json/quizzes/css/advanced/css-advanced-quiz-part3.json (1)

1-234: LGTM! Comprehensive advanced CSS topics.

The quiz data is well-structured with consistent multilingual support. The content covers important advanced CSS concepts including 3D transforms, performance optimization (reflow/repaint, Critical CSS, selector performance), and CSS methodologies (OOCSS, ITCSS). Technical accuracy appears sound.

json/quizzes/css/advanced/css-advanced-quiz-part4.json (1)

1-234: AI summary incorrectly describes TypeScript content; actual content is CSS.

The AI-generated summary mentions TypeScript topics (distributive conditional types, async iterators, Value Objects in DDD, etc.), but the actual file contains CSS questions about aspect-ratio, pseudo-classes, layout techniques, and custom properties. The actual CSS content is well-structured and technically accurate.

json/quizzes/css/advanced/css-advanced-quiz-part2.json (1)

1-234: LGTM! Good coverage of CSS tooling and modern techniques.

The quiz data is properly structured with multilingual support. Content covers important topics including responsive design, CSS animations, preprocessors, modern CSS tooling (Modules, CSS-in-JS, Stylelint), and accessibility features (:focus-visible). Technical accuracy is sound.

json/quizzes/html/advanced/html-advanced-quiz-part4.json (1)

1-434: Well-structured quiz data.

The JSON structure is consistent and well-formed. All questions include proper multilingual support (uk/en/pl), appropriate difficulty levels, and exactly one correct answer per question. The content covers relevant HTML advanced topics effectively.

json/quizzes/js/advanced/javascript-advanced-quiz-part1.json (1)

1-434: Well-structured quiz data.

The JSON structure is consistent and well-formed. All questions include proper multilingual support (uk/en/pl), appropriate difficulty levels, and one correct answer per question. The content effectively covers JavaScript fundamentals through advanced concepts (call/apply/bind, array methods, IIFE, comparison operators, object references).

json/quizzes/html/advanced/html-advanced-quiz-part1.json (1)

1-434: LGTM! Well-structured quiz data.

The HTML advanced quiz part 1 is well-structured with complete multilingual translations (Ukrainian, English, Polish), proper difficulty levels, and technically accurate content. Each question has exactly one correct answer, and the explanations are clear and informative.

json/quizzes/git/git-quiz-part1.json (1)

1-434: LGTM! Comprehensive Git beginner quiz.

The Git quiz part 1 covers fundamental concepts with accurate technical content, complete translations across three languages, and proper structure. Each question has exactly one correct answer and appropriate difficulty levels for beginners.

json/quizzes/git/git-quiz-part4.json (1)

1-434: LGTM! Advanced Git concepts covered well.

Git quiz part 4 provides good coverage of intermediate to advanced Git topics (LFS, shallow clones, submodules, security) with accurate content and complete multilingual support. Structure is consistent and each question has exactly one correct answer.

json/quizzes/html/advanced/html-advanced-quiz-part3.json (1)

1-434: Well-structured advanced HTML quiz with modern web standards.

The HTML advanced quiz part 3 covers important modern web technologies (Web Components, Shadow DOM, Custom Elements, SEO, performance) with complete multilingual translations and proper structure. Content is generally accurate, with one minor clarification needed regarding h1 usage in HTML5.

json/quizzes/css/fundamentals/css-quiz-part3.json (1)

1-434: LGTM! Comprehensive CSS fundamentals coverage.

The CSS quiz part 3 covers essential modern CSS concepts (Grid, Flexbox, media queries, animations, units, variables) with accurate technical content and complete multilingual translations. All 10 questions are properly structured with exactly one correct answer each.

Note: The AI summary mentions "9 questions" but the file actually contains 10 questions (css-q21 through css-q30).

json/quizzes/html/advanced/html-advanced-quiz-part2.json (1)

1-434: LGTM!

The quiz data file is well-structured with consistent multilingual content across all 10 questions. Each question properly includes:

  • Unique ID and order
  • Translations for all three locales (uk/en/pl)
  • Four answer options with exactly one correct answer per question
  • Appropriate difficulty levels
json/quizzes/css/fundamentals/css-quiz-part4.json (1)

1-434: LGTM!

The CSS fundamentals quiz part 4 data is well-structured with accurate CSS content covering modern topics like :has(), @supports, and BEM methodology. All questions have proper multilingual translations and correct answer flags.

json/quizzes/js/advanced/javascript-advanced-quiz-part2.json (1)

1-434: LGTM!

The JavaScript advanced quiz part 2 data is well-structured with accurate JavaScript concepts. The mix of difficulty levels (beginner, intermediate, advanced) within an "advanced" quiz file provides good progression. All translations and correct answer flags are properly set.

json/quizzes/css/fundamentals/css-quiz-part2.json (1)

1-434: LGTM!

The CSS fundamentals quiz part 2 data covers essential CSS concepts like positioning, Flexbox properties, pseudo-classes, and pseudo-elements. Structure is consistent with all required translations and proper answer correctness flags.

json/quizzes/js/advanced/javascript-advanced-quiz-part3.json (1)

1-144: LGTM!

The JavaScript advanced quiz part 3 data is well-structured with accurate JavaScript concepts covering array methods, modern operators, and language features. The compact JSON formatting is valid and consistent.

json/quizzes/css/fundamentals/seed-quiz-css.ts (1)

96-97: Same orphaned records concern applies here.

See comment on seed-quiz-css-advanced.ts lines 96-97 regarding potential orphaned quizQuestionContent, quizAnswers, and quizAnswerTranslations records.

json/quizzes/css/advanced/seed-quiz-css-advanced.ts (1)

96-97: The database schema has ON DELETE CASCADE properly configured for all related tables:

  • quizQuestionContent cascades on quizQuestionId
  • quizAnswers cascades on quizQuestionId
  • quizAnswerTranslations cascades on quizAnswerId

No orphaned records will result from the deletion operations at lines 96-97.

frontend/actions/quiz.ts (6)

65-85: LGTM: Helper functions are well-designed.

Both getQuizQuestionIds and getAnswerRecords are clean, focused helpers that support the batch validation flow. The empty array guard in getAnswerRecords (line 75) is good defensive coding.


91-100: LGTM: Session validation prevents user impersonation.

The session authentication check and userId validation correctly prevent unauthorized submissions and user mismatches.


167-187: LGTM: Time validation prevents suspiciously fast completions.

The date validation and validateTimeSpent check correctly prevent submissions that are completed unrealistically quickly, which helps maintain quiz integrity.


218-228: LGTM: Defensive conditional insert.

The guard if (answerResults.length > 0) is good defensive coding that prevents unnecessary database operations when there are no answer results to persist.


230-246: LGTM: Points awarding and response structure.

The points awarding flow correctly uses session.id and returns a comprehensive result with all relevant metrics including pointsAwarded, totalQuestions, and integrity scores.


87-254: Overall: Excellent validation and security improvements.

The refactored submitQuizAttempt function implements comprehensive validation that addresses multiple security and integrity concerns:

  • Authentication: Session validation prevents unauthorized access
  • Input integrity: Batch validation with duplicate detection and answer-question matching
  • Performance: Batch queries eliminate N+1 problems (reduces queries as stated in PR objectives)
  • Time validation: Prevents unrealistically fast completions
  • Points integration: Properly integrates with the new ledger-based points system

The validation flow is thorough, efficient, and well-structured.

frontend/app/api/quiz/guest-result/route.ts (4)

29-35: LGTM: Session validation for guest results.

The session check correctly ensures that only the authenticated user can submit results for their own userId, preventing unauthorized submissions.


139-170: LGTM: Persistence flow matches quiz.ts pattern.

The persistence logic correctly:

  • Stores pointsEarned in the attempt record (aligns with ledger-based points system)
  • Marks guest submissions with isGuestResult: true in metadata (line 151)
  • Awards points through awardQuizPoints for delta calculation
  • Returns comprehensive result including pointsAwarded

10-188: Overall: Solid guest result validation implementation.

The guest result API route implements comprehensive validation paralleling frontend/actions/quiz.ts:

Strengths:

  • Authentication and authorization checks
  • Thorough input validation with duplicate detection
  • Batch queries for performance
  • Answer-question integrity validation
  • Integration with ledger-based points system
  • Clear isGuestResult flag for auditing

⚠️ Note: Unlike the main quiz submission, this route does not enforce minimum time requirements via validateTimeSpent. Verify this is intentional for guest results.


133-134: Guest result route skips time validation—confirm if this lenient behavior is intentional.

The startedAt timestamp on line 134 is calculated directly from client-provided timeSpentSeconds without enforcing a minimum time window. Unlike frontend/actions/quiz.ts, this route does not call validateTimeSpent, which enforces a minimum of 1 second per question. This means guest results can be submitted with timeSpentSeconds = 0, creating timestamps where startedAt equals completedAt.

The route explicitly marks results with isGuestResult: true in metadata, suggesting this is intentional design. However, if time validation should be consistent across all result types, add the same validateTimeSpent check used in frontend/actions/quiz.ts (lines 51–62 for the function definition, lines 177–187 for usage).

Comment on lines +219 to +260
"id": "html-adv-q26",
"order": 26,
"difficulty": "medium",
"uk": {
"q": "Яке значення мають заголовки h1-h6 для SEO?",
"exp": "<h1> — головний заголовок, має бути один на сторінці, сигналізує основну тему. <h2>-<h6> структурують контент ієрархічно. Використання ключових слів у заголовках покращує релевантність."
},
"en": {
"q": "What importance do h1-h6 headings have for SEO?",
"exp": "<h1> is the main heading, should be one per page, signals main topic. <h2>-<h6> structure content hierarchically. Using keywords in headings improves relevance."
},
"pl": {
"q": "Jakie znaczenie mają nagłówki h1-h6 dla SEO?",
"exp": "<h1> to główny nagłówek, powinien być jeden na stronie, sygnalizuje główny temat. <h2>-<h6> strukturyzują treść hierarchicznie. Używanie słów kluczowych w nagłówkach poprawia trafność."
},
"answers": [
{
"uk": "Заголовки не впливають на SEO",
"en": "Headings don't affect SEO",
"pl": "Nagłówki nie wpływają na SEO",
"correct": false
},
{
"uk": "h1 один на сторінку, ієрархія h2-h6 структурує контент для пошуковиків",
"en": "h1 one per page, h2-h6 hierarchy structures content for search engines",
"pl": "h1 jeden na stronę, hierarchia h2-h6 strukturyzuje treść dla wyszukiwarek",
"correct": true
},
{
"uk": "Можна використовувати кілька h1 на сторінці",
"en": "Can use multiple h1 on a page",
"pl": "Można używać wielu h1 na stronie",
"correct": false
},
{
"uk": "Тільки h1 і h2 впливають на SEO",
"en": "Only h1 and h2 affect SEO",
"pl": "Tylko h1 i h2 wpływają na SEO",
"correct": false
}
]
},
Copy link

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

🌐 Web query:

HTML5 specification multiple h1 elements per page allowed

💡 Result:

Short answer: Yes — the HTML5 specs allow multiple

elements (including one per section in earlier drafts), but current guidance and browser/assistive‑tech practice recommends a single page-level

for clarity and accessibility. [1][2][3]

Sources:

  • WHATWG HTML Living Standard — headings/sections (shows multiple top-level headings and sectioning rules). [1]
  • MDN — heading elements (notes multiple

    allowed by the standard but recommends one

    per page). [2]

  • W3C HTML5 sections documentation (historical spec text describing sectioning and multiple headings). [3]

Update h1 guidance in explanation and correct answer.

The explanation and correct answer state that h1 "should be one per page" as a technical requirement. However, the HTML5 specification allows multiple <h1> elements, particularly when used within sectioning elements like <article>, <section>, <nav>, and <aside>. While using a single <h1> remains the recommended SEO and accessibility best practice, the quiz content presents this as a strict technical rule rather than guidance.

Consider clarifying that one <h1> per page is a best practice for SEO and accessibility, but HTML5 technically permits multiple <h1> elements in certain contexts.

🤖 Prompt for AI Agents
In json/quizzes/html/advanced/html-advanced-quiz-part3.json around lines
219-260, update the question explanation and the correct answer to reflect that
HTML5 technically permits multiple <h1> elements within sectioning elements but
the best practice for SEO and accessibility is to use a single main <h1> per
page; change the "exp" text in uk/en/pl to state this nuance (recommend one per
page for SEO/accessibility, but note HTML5 allows multiple inside
<article>/<section>/<nav>/<aside>), and replace the current correct answer text
("h1 один на сторінку, ієрархія h2-h6...") with a phrasing that marks the
guidance as recommended rather than an absolute rule (e.g., "Рекомендовано одне
h1 на сторінку, але HTML5 допускає кілька в секційних елементах") and keep its
"correct": true flag.

@LesiaUKR LesiaUKR reopened this Jan 3, 2026
Copy link

@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: 1

♻️ Duplicate comments (1)
frontend/proxy.ts (1)

8-11: Critical: JWT signature verification still missing despite previous feedback.

This is the same critical security vulnerability flagged in previous reviews. The decodeAuthToken function decodes and parses the JWT payload without cryptographic signature verification, while AUTH_SECRET is loaded but never used. An attacker can forge tokens with arbitrary claims that would pass validation.

Additionally, there's a type inconsistency: AuthTokenPayload.exp is optional (exp?: number) in lib/auth.ts, but line 27 treats it as required.

The previous review provided a detailed solution using the jose library for proper signature verification. This must be implemented before merging to production.

🔎 Key changes needed
  1. Replace manual JWT parsing with proper signature verification using jose or jsonwebtoken
  2. Make decodeAuthToken async and verify the token signature using AUTH_SECRET
  3. Update isAuthenticated to await the async verification
  4. Align AuthTokenPayload.exp type (make it required if validation requires it)

Also applies to: 13-35

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb93738 and bc9a153.

📒 Files selected for processing (2)
  • frontend/lib/auth.ts
  • frontend/proxy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/lib/auth.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/proxy.ts (1)
frontend/lib/auth.ts (1)
  • AuthTokenPayload (23-28)
🔇 Additional comments (2)
frontend/proxy.ts (2)

4-4: LGTM: Import addresses previous feedback.

The import of AuthTokenPayload from @/lib/auth correctly eliminates the duplicate type definition flagged in previous reviews.


37-46: Logic is sound but requires async signature verification.

The authentication check correctly validates token presence, payload structure, and expiration. However, once decodeAuthToken is updated to perform proper JWT signature verification (per the critical issue above), this function will need to become async and await the verification result.

Ensure that after implementing signature verification:

  • isAuthenticated becomes async
  • Line 41 awaits the verification: const payload = await decodeAuthToken(token);
  • Line 52 in authMiddleware awaits the call: const authenticated = await isAuthenticated(req);

Comment on lines +24 to +28
typeof payload.userId !== "string" ||
(payload.role !== "user" && payload.role !== "admin") ||
typeof payload.email !== "string" ||
typeof payload.exp !== "number"
) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Resolve type inconsistency for exp field.

AuthTokenPayload.exp is defined as optional (exp?: number) in lib/auth.ts, but the validation here treats it as required (line 27). Since expiration validation is essential for token security, make exp required in the type definition.

🔎 Proposed fix in lib/auth.ts

Update the type definition:

export type AuthTokenPayload = {
  userId: string;
  role: "user" | "admin";
  email: string;
  exp: number; // Remove the optional marker
};
🤖 Prompt for AI Agents
In frontend/proxy.ts around lines 24–28 the runtime validation treats
payload.exp as required but AuthTokenPayload in lib/auth.ts defines exp as
optional; update lib/auth.ts to make exp mandatory by removing the optional
marker (change exp?: number to exp: number), then run TypeScript checks and
update any call sites that construct AuthTokenPayload to ensure they supply exp
(or compute/set a proper expiration) so the runtime validation and type
definitions are consistent.

Copy link
Member

@ViktorSvertoka ViktorSvertoka left a comment

Choose a reason for hiding this comment

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

Great job!

@ViktorSvertoka ViktorSvertoka merged commit bb117d1 into develop Jan 3, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the sl/feat/quiz branch January 3, 2026 07:26
@LesiaUKR LesiaUKR restored the sl/feat/quiz branch January 5, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants