Skip to content

feat(quiz): guest results persistence, uuid migration, and quizzes page#69

Closed
LesiaUKR wants to merge 8 commits into
developfrom
sl/feat/quiz
Closed

feat(quiz): guest results persistence, uuid migration, and quizzes page#69
LesiaUKR wants to merge 8 commits into
developfrom
sl/feat/quiz

Conversation

@LesiaUKR
Copy link
Copy Markdown
Collaborator

@LesiaUKR LesiaUKR commented Dec 21, 2025

Quiz flow (guest -> auth)

  • Guest quiz results are temporarily stored in localStorage
  • Added POST /api/quiz/guest-result to persist pending guest results in DB
  • After login/signup, pending results are automatically saved once userId is available
  • Persistence logic is scoped to the Quiz page via PendingResultHandler
  • Guest data is cleared only after successful save
  • Added one-time success feedback via QuizSavedBanner (reads from sessionStorage)
  • Supports JavaScript quizzes (10 added)
  • Added All Quizzes page

Middleware (i18n + auth)

  • Improved i18n + auth middleware behavior
  • Correct handling of locale-aware routes for guest and authenticated users
  • Cleaner separation of access rules without breaking localized routing

Database & schema changes

  • Unified id format across all tables to match quiz tables
  • Migrated foreign keys (including categoryId) from integer - uuid
  • Standardized primary key pattern: uuid('id').defaultRandom().primaryKey()
  • Database now generates UUIDs via gen_random_uuid()

Summary by CodeRabbit

  • New Features

    • Added React and JavaScript quiz collections (40 questions each, multilingual).
    • New quizzes listing page for discovering and starting quizzes.
    • Guest quiz support with offline-preserved results that upload after login.
    • Dashboard banner showing recently saved quiz results and quick actions.
  • Improvements

    • Locale-aware login/signup/dashboard flows and redirects.
    • Enhanced quiz progress UI and improved handling of guest-to-authenticated result flow.

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

  - POST /api/quiz/guest-result saves pending results to DB
  - Login/signup auto-save pending quiz from localStorage
  - Middleware improvements for i18n + auth
  - Component updates for guest flow
  - Unify id format across all tables (consistent with quiz tables)
  - Use uuid('id').defaultRandom().primaryKey() pattern
  - Change categoryId foreign key from integer to uuid
  - DB will generate UUIDs via gen_random_uuid()
- eliminate variable shadowing
- add fetch error handling
- fix ESM __dirname usage in seed scripts
- normalize quiz difficulty values
- minor formatting and redirect logic fixes
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 21, 2025

Walkthrough

Adds guest-quiz support with client-side pending-result persistence, locale-aware routing/redirects, a quizzes listing page, new guest-result API to persist attempts, UUID-based DB schema and seeding scripts, and UI components to surface saved quiz results.

Changes

Cohort / File(s) Summary
Configuration & Ignore
frontend/.claude/settings.local.json, frontend/.gitignore
Removed permissions.allow block from Claude settings; added .claude to .gitignore.
Quiz Data (added)
frontend/data/javascript-quiz-part*.json (1–4), frontend/data/react-quiz-data-part*.json (1–2)
Added multilingual quiz content files (uk/en/pl) for JavaScript and React quizzes.
DB Schema & Queries
frontend/db/schema/categories.ts, frontend/db/queries/quiz.ts
Switched category/question IDs from serial/int to UUID; added getActiveQuizzes(locale) to fetch active quizzes with translations.
DB Seed & Types
frontend/db/seed-quiz-from-json.ts, frontend/db/seed-quiz-javascript.ts, frontend/db/seed-quiz-types.ts
New seed scripts and strong-typed seed types/helpers to populate React and JavaScript quizzes with multi-locale content.
Guest result API
frontend/app/api/quiz/guest-result/route.ts
New POST route to validate, compute and persist guest quiz attempts, insert answers, compute score/integrity, award points, and return attempt summary.
Auth API / Cache
frontend/app/api/auth/{login,signup,logout}/route.ts
Auth routes now include userId in responses and call revalidatePath('/[locale]', 'layout') to revalidate locale layout after auth changes.
Pages — Auth & UX
frontend/app/[locale]/{login,page.tsx,signup/page.tsx}, frontend/app/[locale]/dashboard/page.tsx, frontend/app/[locale]/layout.tsx
Login/signup updated to be locale-aware, preserve returnTo, and upload pending guest results post-auth; dashboard accepts locale params and renders QuizSavedBanner; layout links updated (/quizzes, dashboard link).
Pages — Quizzes
frontend/app/[locale]/quiz/[slug]/page.tsx, frontend/app/[locale]/quizzes/page.tsx
Removed forced unauthenticated redirect for quiz page; added new quizzes listing page quizzes (force-dynamic) showing active quizzes and start links.
Components — Quiz UX
frontend/components/quiz/{QuizContainer,QuizResult,QuizProgress,PendingResultHandler}.tsx
Guest flow: QuizContainer accepts nullable userId and new quizSlug prop; guest submissions saved to localStorage; PendingResultHandler submits pending result when user becomes authenticated; QuizResult supports guest UI; QuizProgress renders ellipses/windowed indices.
Components — Dashboard
frontend/components/dashboard/QuizSavedBanner.tsx
New client component that reads sessionStorage quiz_just_saved and displays a success banner with score and links.
Client Utilities
frontend/lib/guest-quiz.ts, frontend/lib/logout.ts, frontend/proxy.ts
Added localStorage pending quiz persistence with expiry and helpers; removed client-side redirect from logout helper; proxy auth guard narrowed to dashboard-only redirects.
Small / Misc
frontend/actions/quiz.ts
Trailing newline added (no behavioral change).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Browser as Browser (Guest)
    participant QuizComp as Quiz Component
    participant Local as localStorage
    participant AuthPage as Login/Signup Page
    participant API as Server (/api/quiz/guest-result)
    participant DB as Database

    Browser->>QuizComp: Complete quiz (guest)
    QuizComp->>Local: savePendingQuizResult(result)
    QuizComp->>Browser: Redirect to /{locale}/login?returnTo=...
    
    Browser->>AuthPage: Submit credentials
    AuthPage->>API: POST /api/auth/login (receives userId)
    API->>DB: authenticate/create session
    API-->>AuthPage: { userId, ... }
    AuthPage->>Local: getPendingQuizResult()
    alt pending result exists
        AuthPage->>API: POST /api/quiz/guest-result (userId + pending result)
        API->>DB: insert quizAttempt & answers, award points
        API-->>AuthPage: { attemptId, score, pointsAwarded }
        AuthPage->>sessionStorage: save `quiz_just_saved` summary
        AuthPage->>Local: clearPendingQuizResult()
    end
    AuthPage->>Browser: Redirect to /{locale}/dashboard
    Browser->>DashboardComp: mount
    DashboardComp->>sessionStorage: read `quiz_just_saved`
    DashboardComp-->>Browser: render QuizSavedBanner (score, links)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas needing extra attention:
    • frontend/lib/guest-quiz.ts expiry/serialization and SSR guards
    • frontend/app/api/quiz/guest-result/route.ts validation, DB transaction consistency, and awarding logic
    • Seed scripts and UUID migration (frontend/db/*) for ID/foreign-key correctness
    • QuizContainer guest flow, nullable userId handling, and redirects
    • Auth pages' conditional pending-result upload and error handling

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 I hopped with questions, saved them with care,

In storage they rested, waiting there.
A login, a post, a tiny acclaim —
Dashboard unveiled my bright new name.
Hooray for quizzes, locales, and cheer! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.78% 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 directly and specifically summarizes the main changes: guest results persistence, UUID migration, and a new quizzes page.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sl/feat/quiz

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

@netlify
Copy link
Copy Markdown

netlify Bot commented Dec 21, 2025

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit 7c4d9ae
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/69474877cd93920008b50148
😎 Deploy Preview https://deploy-preview-69--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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (16)
frontend/proxy.ts (1)

34-37: LGTM! Reduced auth guard scope to support guest quiz flow.

Allowing unauthenticated access to /quiz and /leaderboard while keeping /dashboard protected aligns with the guest quiz persistence feature. Consider adding a fallback for locale extraction in case the path structure differs:

🔎 Optional defensive locale extraction
  if (pathnameWithoutLocale.startsWith('/dashboard') && !authenticated) {
-   const locale = pathname.split('/')[1];
+   const locale = pathname.split('/')[1] || 'uk';
    return NextResponse.redirect(new URL(`/${locale}/login`, req.url));
  }
frontend/lib/guest-quiz.ts (1)

20-54: Remove or guard debug console.log statements for production.

The implementation is solid with proper SSR guards and expiry handling. However, multiple console.log('[DEBUG]...') statements (lines 22, 25, 27, 32, 37) will clutter production logs and could expose internal state details.

🔎 Recommended: Remove debug logs or use conditional logging
 export function savePendingQuizResult(result: PendingQuizResult): void {
   if (typeof window === 'undefined') {
-    console.log('[DEBUG] savePendingQuizResult: window is undefined (SSR)');
     return;
   }
-  console.log('[DEBUG] savePendingQuizResult: saving to localStorage', result);
   localStorage.setItem(STORAGE_KEY, JSON.stringify(result));
-  console.log('[DEBUG] savePendingQuizResult: saved successfully');
 }

 export function getPendingQuizResult(): PendingQuizResult | null {
   if (typeof window === 'undefined') {
-    console.log('[DEBUG] getPendingQuizResult: window is undefined (SSR)');
     return null;
   }
   
   const stored = localStorage.getItem(STORAGE_KEY);
-  console.log('[DEBUG] getPendingQuizResult: localStorage value =', stored);
   if (!stored) return null;
   // ...
 }
frontend/db/queries/quiz.ts (1)

93-117: Consider adding ORDER BY for consistent quiz listing.

The query correctly fetches active quizzes with locale-specific translations. However, without ordering, the result order is not deterministic, which could cause inconsistent UI rendering.

🔎 Suggested: Add ordering by displayOrder
     .leftJoin(
       quizTranslations,
       and(
         eq(quizTranslations.quizId, quizzes.id),
         eq(quizTranslations.locale, locale)
       )
     )
-    .where(eq(quizzes.isActive, true));
+    .where(eq(quizzes.isActive, true))
+    .orderBy(quizzes.displayOrder);

   return rows;
 }
frontend/components/auth/logoutButton.tsx (1)

3-12: Consider using Next.js router for smoother navigation.

The locale-aware logout flow is correctly implemented, but using window.location.href causes a full page reload. Consider using Next.js router for a smoother client-side transition.

🔎 Optional improvement using Next.js router
 "use client";

-import { useLocale } from 'next-intl';
+import { useRouter } from 'next/navigation';
+import { useLocale } from 'next-intl';
 import { logout } from "@/lib/logout";
 import { Button } from "@/components/ui/button";

 export function LogoutButton() {
+  const router = useRouter();
   const locale = useLocale();
   const handleLogout = async () => {
     await logout();
-    window.location.href = `/${locale}/login`;
+    router.push(`/${locale}/login`);
+    router.refresh();
   };

Note: The current implementation with window.location.href is acceptable if a full page reload is intentionally desired after logout to clear all client state.

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

10-32: Consider extracting shared quiz submission logic.

The POST logic to /api/quiz/guest-result with the same payload structure (userId, quizId, answers, violations, timeSpentSeconds) is duplicated in login/page.tsx, signup/page.tsx, and this component. A shared utility function would reduce duplication and ensure consistent error handling.

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

40-75: Inconsistent indentation and formatting issues.

The code block has mixed indentation (1-space, 2-space, and unindented lines), making it difficult to read. Consider running a formatter like Prettier to normalize the style.

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

46-79: Significant code duplication with login page.

The pending quiz result handling logic (lines 46-79) is nearly identical to the login page. This duplicated code is also present in PendingResultHandler.tsx. Extract this into a shared utility to improve maintainability and ensure consistent behavior.

🔎 Example shared utility
// frontend/lib/submit-pending-quiz.ts
export async function submitPendingQuizResult(
  userId: string,
  pendingResult: PendingQuizResult
): Promise<{ success: boolean; data?: QuizResultData }> {
  const res = await fetch("/api/quiz/guest-result", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      userId,
      quizId: pendingResult.quizId,
      answers: pendingResult.answers,
      violations: pendingResult.violations,
      timeSpentSeconds: pendingResult.timeSpentSeconds,
    }),
  });
  
  if (!res.ok) {
    throw new Error(`API error: ${res.status}`);
  }
  
  const result = await res.json();
  if (result.success) {
    sessionStorage.setItem('quiz_just_saved', JSON.stringify({
      score: result.score,
      total: result.totalQuestions,
      percentage: result.percentage,
      pointsAwarded: result.pointsAwarded,
      quizSlug: pendingResult.quizSlug,
    }));
  }
  
  clearPendingQuizResult();
  return { success: result.success, data: result };
}

24-87: Inconsistent indentation and formatting.

Similar to the login page, this code block has inconsistent indentation. Consider running a formatter to normalize the style.

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

16-19: Hardcoded UI strings in a locale-aware page.

This page uses locale for fetching localized quiz data but renders UI text in English (lines 17-18, 28-34, 55, 67). Consider using next-intl translations for consistency with the locale-aware routing pattern established in this PR.

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

82-86: Log errors before returning generic response.

The error is caught but discarded, making debugging difficult. Consider logging the error while returning a safe message to the client.

🔎 Proposed fix
   } catch (error) {
+    console.error('Failed to save guest result:', error);
     return NextResponse.json(
       { success: false, error: "Failed to save result" },
       { status: 500 }
     );
   }

30-30: Avoid unnecessary string-to-number conversion.

percentage is formatted as a string with toFixed(2) on line 30, then parsed back to float on line 78. Consider keeping it as a number throughout.

🔎 Proposed fix
-    const percentage = ((correctAnswersCount / totalQuestions) * 100).toFixed(2);
+    const percentage = (correctAnswersCount / totalQuestions) * 100;
     // ...
-        percentage: parseFloat(percentage),
+        percentage: Math.round(percentage * 100) / 100,
frontend/db/seed-quiz-javascript.ts (1)

21-22: Reuse the shared Locale type.

The Locale type is already defined in seed-quiz-types.ts. Import it to maintain consistency and reduce duplication.

🔎 Proposed fix
-type Locale = 'uk' | 'en' | 'pl';
-const LOCALES: Locale[] = ['uk', 'en', 'pl'];
+import { Locale } from './seed-quiz-types';
+const LOCALES: Locale[] = ['uk', 'en', 'pl'];
frontend/components/quiz/QuizResult.tsx (2)

101-114: Use Link component or router.push instead of window.location.href.

Using window.location.href triggers a full page reload, losing React state and client-side navigation benefits. The Link component from @/i18n/routing is already used elsewhere in the codebase for locale-aware navigation.

Additionally, quizSlug should be URL-encoded to handle special characters safely.

🔎 Proposed fix using Link
+import { Link } from '@/i18n/routing';
+
 <div className="flex flex-col sm:flex-row gap-3 justify-center">
-  <Button
-    onClick={() => window.location.href = `/${locale}/login?returnTo=/quiz/${quizSlug}`}
-    variant="primary"
-  >
-    Увійти
-  </Button>
-  <Button
-    onClick={() => window.location.href = `/${locale}/signup?returnTo=/quiz/${quizSlug}`}
-    variant="secondary"
-  >
-    Зареєструватися
-  </Button>
+  <Button asChild variant="primary">
+    <Link href={`/login?returnTo=/quiz/${encodeURIComponent(quizSlug)}`}>
+      Увійти
+    </Link>
+  </Button>
+  <Button asChild variant="secondary">
+    <Link href={`/signup?returnTo=/quiz/${encodeURIComponent(quizSlug)}`}>
+      Зареєструватися
+    </Link>
+  </Button>
 </div>

94-125: Inconsistent indentation in the conditional block.

The code block from lines 95-115 has inconsistent indentation compared to the rest of the component. Consider reformatting for consistency.

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

210-210: Full page reload for back navigation.

Using window.location.href triggers a full page reload. Consider using Next.js router (useRouter from next/navigation) for client-side navigation to preserve SPA behavior and improve performance.

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

1-2: Path in comment doesn't match actual file location.

The comment references db/seeds/seed-quiz-types.ts but the file is located at frontend/db/seed-quiz-types.ts.

🔎 Proposed fix
-// db/seeds/seed-quiz-types.ts
+// frontend/db/seed-quiz-types.ts
 // Types and interfaces for quiz seed data
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d80f95 and 5f58310.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • frontend/.claude/settings.local.json (0 hunks)
  • frontend/.gitignore (1 hunks)
  • frontend/actions/quiz.ts (1 hunks)
  • frontend/app/[locale]/dashboard/page.tsx (3 hunks)
  • frontend/app/[locale]/layout.tsx (2 hunks)
  • frontend/app/[locale]/login/page.tsx (3 hunks)
  • frontend/app/[locale]/quiz/[slug]/page.tsx (2 hunks)
  • frontend/app/[locale]/quizzes/page.tsx (1 hunks)
  • frontend/app/[locale]/signup/page.tsx (3 hunks)
  • frontend/app/api/auth/login/route.ts (2 hunks)
  • frontend/app/api/auth/logout/route.ts (1 hunks)
  • frontend/app/api/auth/signup/route.ts (2 hunks)
  • frontend/app/api/quiz/guest-result/route.ts (1 hunks)
  • frontend/components/auth/logoutButton.tsx (1 hunks)
  • frontend/components/dashboard/QuizSavedBanner.tsx (1 hunks)
  • frontend/components/quiz/PendingResultHandler.tsx (1 hunks)
  • frontend/components/quiz/QuizContainer.tsx (7 hunks)
  • frontend/components/quiz/QuizProgress.tsx (2 hunks)
  • frontend/components/quiz/QuizResult.tsx (4 hunks)
  • frontend/data/javascript-quiz-part1.json (1 hunks)
  • frontend/data/javascript-quiz-part2.json (1 hunks)
  • frontend/data/javascript-quiz-part3.json (1 hunks)
  • frontend/data/javascript-quiz-part4.json (1 hunks)
  • frontend/data/react-quiz-data-part1.json (1 hunks)
  • frontend/data/react-quiz-data-part2.json (1 hunks)
  • frontend/db/queries/quiz.ts (1 hunks)
  • frontend/db/schema/categories.ts (1 hunks)
  • frontend/db/seed-quiz-from-json.ts (1 hunks)
  • frontend/db/seed-quiz-javascript.ts (1 hunks)
  • frontend/db/seed-quiz-types.ts (1 hunks)
  • frontend/lib/guest-quiz.ts (1 hunks)
  • frontend/lib/logout.ts (1 hunks)
  • frontend/proxy.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/.claude/settings.local.json
🧰 Additional context used
🧬 Code graph analysis (19)
frontend/db/queries/quiz.ts (3)
frontend/db/index.ts (1)
  • db (17-17)
frontend/db/schema/quiz.ts (3)
  • quizzes (18-38)
  • quizTranslations (40-53)
  • translations (189-193)
frontend/drizzle/relations.ts (1)
  • quiz (88-93)
frontend/app/api/quiz/guest-result/route.ts (3)
frontend/db/index.ts (1)
  • db (17-17)
frontend/db/queries/points.ts (1)
  • awardQuizPoints (49-94)
frontend/db/schema/quiz.ts (1)
  • attempt (265-278)
frontend/components/quiz/PendingResultHandler.tsx (1)
frontend/lib/guest-quiz.ts (2)
  • getPendingQuizResult (30-54)
  • clearPendingQuizResult (56-59)
frontend/db/seed-quiz-javascript.ts (3)
frontend/db/seed-quiz-types.ts (1)
  • Locale (4-4)
frontend/db/index.ts (1)
  • db (17-17)
frontend/db/schema/quiz.ts (2)
  • quiz (197-202)
  • quiz (207-215)
frontend/app/api/auth/signup/route.ts (1)
frontend/lib/auth.ts (1)
  • clearAuthCookie (88-91)
frontend/data/javascript-quiz-part1.json (3)
frontend/db/seed-quiz-react.ts (2)
  • QuizQuestionSeed (30-46)
  • seedReactQuiz (880-965)
frontend/db/schema/quiz.ts (3)
  • quiz (197-202)
  • quiz (207-215)
  • question (228-235)
frontend/db/verify-quiz-seed.ts (2)
  • q (165-176)
  • verifyQuizSeed (12-123)
frontend/components/quiz/QuizProgress.tsx (1)
frontend/lib/utils.ts (1)
  • cn (4-6)
frontend/components/dashboard/QuizSavedBanner.tsx (1)
frontend/components/leaderboard/LeaderboardClient.tsx (1)
  • LeaderboardClient (14-91)
frontend/app/[locale]/login/page.tsx (2)
frontend/lib/guest-quiz.ts (2)
  • getPendingQuizResult (30-54)
  • clearPendingQuizResult (56-59)
frontend/components/shared/LanguageSwitcher.tsx (2)
  • locale (53-67)
  • LanguageSwitcher (8-72)
frontend/app/[locale]/quizzes/page.tsx (2)
frontend/app/[locale]/layout.tsx (1)
  • dynamic (15-15)
frontend/db/queries/quiz.ts (1)
  • getActiveQuizzes (93-117)
frontend/app/[locale]/quiz/[slug]/page.tsx (2)
frontend/drizzle/schema.ts (1)
  • user (119-130)
frontend/components/quiz/PendingResultHandler.tsx (1)
  • PendingResultHandler (10-35)
frontend/data/javascript-quiz-part2.json (3)
frontend/db/schema/quiz.ts (3)
  • quiz (197-202)
  • quiz (207-215)
  • question (228-235)
frontend/db/seed-quiz-react.ts (2)
  • seedReactQuiz (880-965)
  • QuizQuestionSeed (30-46)
frontend/db/verify-quiz-seed.ts (1)
  • verifyQuizSeed (12-123)
frontend/db/schema/categories.ts (4)
frontend/db/schema/quiz.ts (3)
  • quiz (207-215)
  • question (228-235)
  • table (87-89)
frontend/drizzle/relations.ts (2)
  • category (4-9)
  • quizQuestion (81-86)
frontend/drizzle/schema.ts (2)
  • table (108-117)
  • table (180-187)
frontend/db/seed.ts (1)
  • main (19-50)
frontend/components/quiz/QuizContainer.tsx (1)
frontend/lib/guest-quiz.ts (1)
  • savePendingQuizResult (20-28)
frontend/components/auth/logoutButton.tsx (1)
frontend/lib/logout.ts (1)
  • logout (3-9)
frontend/app/[locale]/dashboard/page.tsx (4)
frontend/lib/auth.ts (1)
  • getCurrentUser (94-115)
frontend/drizzle/schema.ts (1)
  • user (119-130)
frontend/db/queries/users.ts (1)
  • getUserProfile (7-28)
frontend/components/dashboard/QuizSavedBanner.tsx (1)
  • QuizSavedBanner (14-59)
frontend/app/api/auth/logout/route.ts (3)
frontend/app/api/auth/login/route.ts (1)
  • POST (18-62)
frontend/app/api/auth/signup/route.ts (1)
  • POST (19-71)
frontend/lib/auth.ts (1)
  • clearAuthCookie (88-91)
frontend/db/seed-quiz-from-json.ts (2)
frontend/db/seed-quiz-types.ts (1)
  • Locale (4-4)
frontend/db/index.ts (1)
  • db (17-17)
frontend/app/[locale]/signup/page.tsx (1)
frontend/lib/guest-quiz.ts (2)
  • getPendingQuizResult (30-54)
  • clearPendingQuizResult (56-59)
🔇 Additional comments (33)
frontend/.gitignore (1)

49-49: LGTM—add .claude to gitignore.

Adding .claude to exclude local Claude tool configuration and artifacts is appropriate. The pattern correctly aligns with the Claude-related configuration changes elsewhere in the PR (e.g., frontend/.claude/settings.local.json). Note that the pattern without a trailing slash will match both files and directories named .claude; if you want to be more explicit about ignoring only the directory, use .claude/ instead.

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

1-1: LGTM: Import changes align with UUID migration.

The import statement correctly reflects the migration from integer-based to UUID-based primary keys.


18-27: LGTM: Relations are correctly defined.

The relations between categories and questions are properly structured with no functional changes. The references will work correctly with the new UUID-based primary keys.

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

2-7: LGTM! Consistent cache revalidation after auth state change.

This aligns with the same pattern used in the login and signup routes, ensuring the locale-aware layout is revalidated after logout so user-dependent UI elements (e.g., Dashboard vs Login links) reflect the updated auth state.

frontend/data/react-quiz-data-part2.json (1)

1-244: Well-structured multilingual quiz data.

The questions are comprehensive, covering React hooks, rendering, lifecycle methods, and advanced concepts. Structure is consistent with proper multilingual support.

One observation: this file uses "medium" for difficulty (e.g., line 4), while javascript-quiz-part3.json uses "intermediate". Consider standardizing the difficulty enum across all quiz data files to avoid potential UI/filtering inconsistencies.

frontend/data/javascript-quiz-part3.json (1)

1-434: LGTM! Well-structured JavaScript quiz data covering async patterns.

The questions provide comprehensive coverage of Promises, async/await, Event Loop, and error handling - all important JavaScript concepts. Multilingual content is complete and answer structures are consistent.

frontend/app/[locale]/layout.tsx (2)

64-69: LGTM! Quiz navigation updated to new quizzes listing page.

Changing from /quiz/react-fundamentals to /quizzes properly routes to the new All Quizzes page, enabling users to browse available quizzes.


109-112: LGTM! Dashboard link added for authenticated users.

Good addition - the Dashboard link is logically placed before the LogoutButton, and the fragment correctly groups the elements without adding extra DOM nodes.

frontend/actions/quiz.ts (1)

171-172: LGTM!

Trailing newline added - no functional changes.

frontend/lib/logout.ts (1)

3-9: LGTM! Navigation responsibility correctly moved to caller.

The removal of the client-side redirect is intentional and aligns with the PR's locale-aware routing strategy. Navigation is now handled by the calling component (logoutButton.tsx), which constructs the locale-aware path /${locale}/login.

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

69-70: LGTM! Locale layout revalidation and userId return enable post-signup flows.

The revalidatePath call invalidates the locale-aware layout cache after signup, ensuring the UI reflects the authenticated state. Returning userId enables the client to persist pending guest quiz results.


2-2: Remove request_verification from the review comment — no issues found.

The import and usage of revalidatePath with the 'layout' type parameter are fully compatible with Next.js 15. The 'layout' type parameter is an optional parameter that changes the type of path to revalidate, and examples in the Next.js documentation show revalidatePath('/', 'layout'). The code on line 69 calls revalidatePath('/[locale]', 'layout') within a Route Handler's POST method, which is the correct usage context. No verification is needed.

Likely an incorrect or invalid review comment.

frontend/app/api/auth/login/route.ts (2)

2-2: Consistent import for locale-aware cache invalidation.

Matches the pattern in signup/route.ts.


60-61: LGTM! Consistent post-login cache invalidation and userId response.

The implementation mirrors the signup route, ensuring locale-aware layout refresh and enabling pending guest quiz result persistence.

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

54-59: LGTM! Guest quiz flow correctly implemented.

The changes enable guests to take quizzes while ensuring pending results are submitted after authentication:

  • quizSlug prop enables result tracking
  • Nullable userId supports guest users
  • PendingResultHandler conditionally submits pending results when user is present
frontend/components/auth/logoutButton.tsx (1)

14-16: LGTM! Button correctly triggers locale-aware logout.

frontend/components/dashboard/QuizSavedBanner.tsx (1)

43-54: LGTM! Locale-aware navigation links correctly implemented.

The links use the locale-aware Link component from @/i18n/routing, ensuring proper navigation within the user's selected locale.

frontend/data/javascript-quiz-part1.json (1)

1-434: JSON structure is valid and complete.

All verification checks passed: 10 questions with proper structure, each containing 4 answer options and exactly 1 correct answer. All questions include Ukrainian, English, and Polish localizations with sequential ordering (1-10). No ID conflicts found with other quiz files.

frontend/data/javascript-quiz-part4.json (1)

1-434: JSON structure is valid and complete for the javascript-quiz-part4 quiz dataset.

All 10 questions (js-q31 through js-q40) pass structural validation. Each question includes the required fields (id, order, difficulty, uk, en, pl locales with q and exp fields, and answers array). All 10 questions have exactly 4 answers with 1 correct answer per question. The order sequence is sequential from 31 to 40. All three language locales (Ukrainian, English, Polish) are present for every question.

frontend/data/javascript-quiz-part2.json (2)

1-434: LGTM on quiz content structure and translations.

The quiz data structure is well-organized with consistent multilingual support across all 10 questions. Each question correctly has one correct answer and proper explanations in all three locales.


6-6: No changes required. The "intermediate" difficulty values in javascript-quiz-part2.json are correct and match the JavaScript seed script schema (frontend/db/seed-quiz-javascript.ts), which accepts 'beginner' | 'intermediate' | 'advanced'. The file does not need modification.

Likely an incorrect or invalid review comment.

frontend/data/react-quiz-data-part1.json (1)

12-252: LGTM on React quiz content.

The quiz questions are well-structured with consistent difficulty levels (beginner, medium) that match the expected schema. The multilingual content is complete and accurate.

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

3-7: LGTM on imports and locale-aware routing setup.

The new imports for useLocale, Link, useSearchParams, and guest-quiz utilities are appropriate for the enhanced login flow with locale awareness and pending result handling.

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

3-7: LGTM on imports.

The imports are appropriate and consistent with the login page's locale-aware routing and pending result handling requirements.

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

16-46: LGTM!

The getVisibleIndices helper correctly implements a pagination-like display with ellipsis for large quiz totals (>7 questions). The edge cases for first/last positions and neighbor calculations are handled properly.

frontend/app/[locale]/dashboard/page.tsx (2)

43-49: Redundant null coalescing for already nullable fields.

user.name ?? null and user.createdAt ?? null are likely redundant if these fields are already typed as nullable. However, this defensive approach doesn't cause issues.


89-89: QuizSavedBanner integration looks correct.

The client component is properly placed in the server component hierarchy and will hydrate correctly to read from sessionStorage.

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

55-60: questionsCount is non-nullable; timeLimitSeconds already has fallback handling.

The questionsCount field cannot be null, so no null check is needed there. The timeLimitSeconds field can be null, but the existing conditional {quiz.timeLimitSeconds && (...)} already handles both null and zero cases by hiding the element when falsy. No additional fallback handling is required.

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

3-4: LGTM!

Clean imports for locale handling and guest quiz persistence.


99-100: LGTM!

Good approach to support guest flows by making userId nullable and deriving isGuest from it. The quizSlug addition enables proper identification for pending result storage.

Also applies to: 105-105, 120-120, 122-122


288-289: LGTM!

Correctly propagates guest state and quiz slug to QuizResult for conditional UI rendering.

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

4-35: LGTM!

Well-structured type definitions with proper use of discriminated unions for block types, optional fields for styling variants, and Record<Locale, ...> for localization support. The QuizQuestionSeed interface provides a clean schema for multilingual quiz content.


37-66: LGTM!

Clean helper functions that provide a nice DSL for constructing rich content blocks. The as const assertion on 'listItem' ensures proper type narrowing.

Comment thread frontend/app/[locale]/login/page.tsx Outdated
Comment thread frontend/app/[locale]/login/page.tsx Outdated
timeSpentSeconds: pendingResult.timeSpentSeconds,
}),
});
const result = await res.json();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing response status check before parsing JSON.

res.json() is called without first checking if the response was successful. If the API returns an error status, parsing may fail or produce unexpected data. Check res.ok before proceeding.

🔎 Proposed fix
     });
-    const result = await res.json();
+    if (!quizRes.ok) {
+      console.error('Guest-result API error:', quizRes.status);
+      return;
+    }
+    const result = await quizRes.json();
📝 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 result = await res.json();
if (!res.ok) {
console.error('Guest-result API error:', res.status);
return;
}
const result = await res.json();
🤖 Prompt for AI Agents
In frontend/app/[locale]/login/page.tsx around line 55, the code calls
res.json() without checking the HTTP response status; update the flow to first
check res.ok (or res.status) and handle non-2xx responses (throw a descriptive
error or read error body) before attempting to parse JSON, so only successful
responses are parsed and errors are surfaced/handled appropriately.

Comment thread frontend/app/[locale]/login/page.tsx Outdated
Comment on lines +72 to +73
window.location.href = `/${locale}/dashboard`;
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

returnTo is ignored when a pending quiz result exists.

When a pending quiz result is saved, the user is always redirected to the dashboard (line 72), ignoring any returnTo value. This may cause unexpected navigation behavior—users who expected to return to a specific page after login will instead land on the dashboard.

🔎 Proposed fix
-  window.location.href = `/${locale}/dashboard`;
+  window.location.href = returnTo || `/${locale}/dashboard`;
📝 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
window.location.href = `/${locale}/dashboard`;
return;
window.location.href = returnTo || `/${locale}/dashboard`;
return;
🤖 Prompt for AI Agents
In frontend/app/[locale]/login/page.tsx around lines 72 to 73, the code always
does window.location.href = `/${locale}/dashboard`; which forces a dashboard
redirect even when a returnTo value exists; update the redirect logic to use the
returnTo value when present and valid (e.g. const dest = returnTo &&
isSafeReturn(returnTo) ? returnTo : `/${locale}/dashboard`) and set
window.location.href = dest so pending quiz saving still occurs but the user is
returned to the intended page; validate/sanitize returnTo to prevent
open-redirects (allow only same-origin or relative paths).

Comment thread frontend/app/[locale]/quiz/[slug]/page.tsx Outdated
Comment thread frontend/app/[locale]/signup/page.tsx Outdated
interface QuestionData {
id: string;
order: number;
difficulty: 'beginner' | 'medium' | 'advanced';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find schema files and seed files
fd -t f -e ts -e prisma | grep -E "(schema|seed)" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 671


🏁 Script executed:

#!/bin/bash
# Look for seed files and schema definitions
find . -type f \( -name "*.ts" -o -name "*.prisma" \) | grep -iE "(seed|schema)" | head -30

Repository: DevLoversTeam/devlovers.net

Length of output: 775


🏁 Script executed:

#!/bin/bash
# Search for the seed files
find . -type f -name "*seed-quiz*" 
# Also search for schema/model definitions
find . -type f -name "schema.prisma" -o -name "*.prisma"
# Search for difficulty type definitions
rg "difficulty.*beginner.*medium.*advanced|difficulty.*beginner.*intermediate.*advanced" --type ts -A 2 -B 2

Repository: DevLoversTeam/devlovers.net

Length of output: 1482


🏁 Script executed:

#!/bin/bash
# Read the quiz schema to see what difficulty values are defined
cat -n frontend/db/schema/quiz.ts | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3614


Standardize difficulty value to 'medium' across seed files.

seed-quiz-javascript.ts uses 'intermediate' for the middle difficulty level, while this file and others use 'medium'. Align the values to ensure consistent data representation across all quiz seeds.

🤖 Prompt for AI Agents
In frontend/db/seed-quiz-from-json.ts around line 32, ensure the difficulty
union and all difficulty values use 'medium' as the standardized middle level;
update the type to 'beginner' | 'medium' | 'advanced' (or leave as-is if already
that) and scan this file for any 'intermediate' literals and replace them with
'medium' so seeding data and types match the rest of the codebase.

Comment on lines +51 to +52
const part1Path = join(__dirname, '../data/react-quiz-data-part1.json');
const part2Path = join(__dirname, '../data/react-quiz-data-part2.json');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check package.json for type field
echo "=== Checking package.json for ESM configuration ==="
cat package.json | grep -A 2 -B 2 '"type"'

# Check lines 51-52 in the target file
echo ""
echo "=== Checking seed-quiz-from-json.ts lines 51-52 ==="
if [ -f "frontend/db/seed-quiz-from-json.ts" ]; then
  sed -n '45,55p' frontend/db/seed-quiz-from-json.ts | cat -n
else
  echo "File not found"
fi

# Search for all __dirname usage
echo ""
echo "=== Searching for __dirname usage ==="
rg -n "__dirname" --type ts frontend/

# Check seed-quiz-javascript.ts for comparison
echo ""
echo "=== Checking seed-quiz-javascript.ts for path handling ==="
if [ -f "frontend/db/seed-quiz-javascript.ts" ]; then
  rg -n -B 2 -A 2 "process.cwd\|__dirname" frontend/db/seed-quiz-javascript.ts
else
  echo "File not found"
fi

Repository: DevLoversTeam/devlovers.net

Length of output: 1347


🏁 Script executed:

#!/bin/bash

# Find package.json files
echo "=== Finding package.json files ==="
find . -name "package.json" -type f | head -20

# Check for tsconfig.json
echo ""
echo "=== Finding tsconfig files ==="
find . -name "tsconfig*.json" -type f | head -20

# Get more context from seed-quiz-from-json.ts beginning
echo ""
echo "=== Checking imports in seed-quiz-from-json.ts ==="
head -20 frontend/db/seed-quiz-from-json.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 742


🏁 Script executed:

#!/bin/bash

# Check frontend/package.json for type field and relevant config
echo "=== Contents of frontend/package.json ==="
cat frontend/package.json

echo ""
echo "=== Checking frontend/tsconfig.json ==="
cat frontend/tsconfig.json

Repository: DevLoversTeam/devlovers.net

Length of output: 2887


__dirname is not available in ES module context.

The file uses import statements and is executed via tsx, which treats it as an ESM module. In ESM, __dirname is undefined. Use import.meta.url with fileURLToPath instead:

+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
-  const part1Path = join(__dirname, '../data/react-quiz-data-part1.json');
+  const part1Path = join(__dirname, 'data/react-quiz-data-part1.json');

Alternatively, use process.cwd() as in frontend/parse/parse.ts.

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

🤖 Prompt for AI Agents
In frontend/db/seed-quiz-from-json.ts around lines 51-52, the code uses
__dirname to build paths but this file is an ES module (import/tsx) where
__dirname is undefined; replace __dirname usage by deriving the current file
directory from import.meta.url via fileURLToPath and path.dirname (or, if
consistent with other scripts, use process.cwd()) and use that directory when
calling join to construct part1Path and part2Path so the paths resolve correctly
in ESM.

Comment thread frontend/db/seed-quiz-from-json.ts Outdated
Comment on lines +84 to +87
if (existing) {
await db.delete(quizzes).where(eq(quizzes.id, existing.id));
console.log('Old quiz deleted');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Deleting existing quiz cascades to user attempt history.

Deleting the quiz by ID will cascade-delete all associated quizAttempts and quizAttemptAnswers, permanently losing user quiz history. Consider an upsert pattern or versioning strategy to preserve historical data.

🤖 Prompt for AI Agents
In frontend/db/seed-quiz-javascript.ts around lines 84 to 87, the current code
deletes an existing quiz which cascades and permanently removes associated
quizAttempts and quizAttemptAnswers; instead preserve history by avoiding
cascade delete — implement an upsert or versioning approach: if a quiz exists,
update its fields (or mark it inactive) and insert a new quiz row with a new
version identifier (or increment a version column) for the new content, or copy
the existing record to an archive table before modifying; ensure any FK
constraints prevent cascade deletes (or disable cascade on delete) so user
attempt rows remain intact and adjust seed logic to perform an update/insert
rather than deletion.

Comment on lines +89 to +97
// Insert quiz - DB generates id
const [quiz] = await db.insert(quizzes).values({
topicId: randomUUID(),
slug: QUIZ_METADATA.slug,
displayOrder: 1,
questionsCount: QUIZ_METADATA.questionsCount,
timeLimitSeconds: QUIZ_METADATA.timeLimitSeconds,
isActive: true,
}).returning();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find database-related files in the repository
find . -type f \( -name "schema.ts" -o -name "*schema*" -o -name "*db*" \) | grep -E "(frontend|db)" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 963


🏁 Script executed:

#!/bin/bash
# Search for topics and quizzes table definitions
rg -n "topics|quizzes" --type=ts -l | grep -E "(schema|db)" | head -10

Repository: DevLoversTeam/devlovers.net

Length of output: 286


🏁 Script executed:

#!/bin/bash
# Look at the seed file in question
cat -n frontend/db/seed-quiz-javascript.ts 2>/dev/null || find . -name "seed-quiz-javascript.ts" -exec cat -n {} \;

Repository: DevLoversTeam/devlovers.net

Length of output: 8242


🏁 Script executed:

#!/bin/bash
# Read the quiz schema
cat -n frontend/db/schema/quiz.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 9779


🏁 Script executed:

#!/bin/bash
# Check the main drizzle schema
cat -n frontend/drizzle/schema.ts | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 4909


🏁 Script executed:

#!/bin/bash
# Look for topics table definition
rg -n "topics" frontend/drizzle/schema.ts frontend/db/schema/

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Search for 'topics' or 'topic' in all TypeScript files
rg -i "topics\|\.topics\|topic_id" --type=ts | head -30

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Check if there's a topics table definition anywhere
rg -n "pgTable.*topic" --type=ts

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Look for other seed files to understand the pattern
ls -la frontend/db/seed*.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 717


🏁 Script executed:

#!/bin/bash
# Check the main seed.ts file
cat -n frontend/db/seed.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1621


🏁 Script executed:

#!/bin/bash
# Check the seed-quiz-react.ts to see if same pattern
grep -n "topicId" frontend/db/seed-quiz-react.ts | head -10

Repository: DevLoversTeam/devlovers.net

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Check seed-quiz-from-json.ts
grep -n "topicId\|randomUUID" frontend/db/seed-quiz-from-json.ts | head -15

Repository: DevLoversTeam/devlovers.net

Length of output: 163


🏁 Script executed:

#!/bin/bash
# Check the react seed file to see TOPIC_ID definition
head -100 frontend/db/seed-quiz-react.ts | grep -A5 -B5 "TOPIC_ID"

Repository: DevLoversTeam/devlovers.net

Length of output: 352


topicId uses a random UUID instead of referencing an existing topic.

The topicId column in the quizzes table has a .notNull() constraint but uses randomUUID() on every run instead of referencing a valid topic. While QUIZ_METADATA defines topicId: 'topic-javascript', this value is never used. Additionally, no topics table exists in the schema and no foreign key constraint is defined. This creates orphaned quiz records without valid topic references. Either create and seed a topics table with proper foreign key constraints, or reconsider the design if topics should be optional.

🤖 Prompt for AI Agents
In frontend/db/seed-quiz-javascript.ts around lines 89 to 97, the quiz insertion
uses randomUUID() for topicId which creates orphaned records; replace
randomUUID() with the intended QUIZ_METADATA.topicId and ensure a corresponding
topic row is created in the topics table before inserting quizzes (or, if topics
are optional, alter the schema to allow nullable topicId and remove/avoid a
foreign key). Seed the topics table with an entry whose id matches
QUIZ_METADATA.topicId (and any required columns) prior to inserting the quiz, or
adjust the schema and QUIZ_METADATA to reflect that topicId can be absent.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
frontend/db/seed-quiz-javascript.ts (1)

44-44: Random UUID for topic creates orphaned quiz records.

This issue was flagged in a previous review and remains unresolved. Line 44 generates a new random UUID on every script run, and line 122 uses this random value when creating a new quiz. This creates quiz records with topicId values that don't reference any existing topic in the database, resulting in orphaned records without valid topic references.

Either create and seed a topics table with proper foreign key constraints before inserting quizzes, or make topicId nullable in the schema if topics are optional.

🔎 Proposed fix: use a deterministic topic ID

If a topics table exists or will be created:

-const TOPIC_ID = randomUUID();
+// Use deterministic UUID for 'javascript' topic or retrieve from topics table
+const TOPIC_ID = 'predefined-uuid-for-javascript-topic'; // or fetch from DB

Alternatively, if topics are optional:

-const TOPIC_ID = randomUUID();
+const TOPIC_ID = null; // and update schema to allow nullable topicId
🧹 Nitpick comments (1)
frontend/db/seed-quiz-javascript.ts (1)

74-78: Consider adding file existence validation.

The readFileSync call will throw if the JSON file is missing, but the error won't clearly indicate which file was expected. Adding validation or a more descriptive error message would improve developer experience.

🔎 Optional improvement
 async function loadQuestions(partNumber: number): Promise<QuestionData[]> {
   const partPath = join(process.cwd(), 'data', `javascript-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;
 }

Note: You'll need to import existsSync from 'fs'.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f58310 and 7c4d9ae.

📒 Files selected for processing (9)
  • frontend/app/[locale]/login/page.tsx (3 hunks)
  • frontend/app/[locale]/quiz/[slug]/page.tsx (2 hunks)
  • frontend/app/[locale]/signup/page.tsx (2 hunks)
  • frontend/data/javascript-quiz-part1.json (1 hunks)
  • frontend/data/javascript-quiz-part2.json (1 hunks)
  • frontend/data/javascript-quiz-part3.json (1 hunks)
  • frontend/data/javascript-quiz-part4.json (1 hunks)
  • frontend/db/seed-quiz-from-json.ts (1 hunks)
  • frontend/db/seed-quiz-javascript.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • frontend/app/[locale]/signup/page.tsx
  • frontend/data/javascript-quiz-part4.json
  • frontend/app/[locale]/login/page.tsx
  • frontend/data/javascript-quiz-part2.json
  • frontend/data/javascript-quiz-part3.json
  • frontend/db/seed-quiz-from-json.ts
  • frontend/data/javascript-quiz-part1.json
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/[locale]/quiz/[slug]/page.tsx (2)
frontend/components/quiz/PendingResultHandler.tsx (1)
  • PendingResultHandler (10-35)
frontend/components/quiz/QuizContainer.tsx (2)
  • QuizContainerProps (92-97)
  • QuizContainer (99-278)
🔇 Additional comments (5)
frontend/app/[locale]/quiz/[slug]/page.tsx (3)

4-5: Past review comment addressed.

The import spacing issue flagged in the previous review has been fixed. The import statement now has proper spacing.


59-59: LGTM: Correct conditional rendering for guest result persistence.

The PendingResultHandler is correctly rendered only when a user is authenticated, ensuring that pending guest quiz results are automatically saved after login/signup. Since the component is inside the {user && ...} guard, user.id is guaranteed to be available.


53-58: QuizContainer props are correctly typed.

The QuizContainer component interface (frontend/components/quiz/QuizContainer.tsx, lines 96-102) confirms it accepts:

  • quizSlug: string
  • userId: string | null

These match the props being passed in the page component and support the guest quiz flow where userId can be null.

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

88-93: Good improvement: attempt check prevents data loss.

The addition of this check addresses the cascade delete concern from the previous review. By throwing an error when attempts exist, the script now prevents accidental deletion of user quiz history.


186-239: LGTM: Clear command-line interface with good validation.

The argument parsing, validation, and error messaging are well-implemented. The support for both single-part and all-parts seeding provides good flexibility.

Comment on lines +95 to +115
await db.delete(quizQuestions).where(eq(quizQuestions.quizId, existing.id));
await db.delete(quizTranslations).where(eq(quizTranslations.quizId, existing.id));
await db.update(quizzes).set({
topicId: existing.topicId,
slug: QUIZ_METADATA.slug,
displayOrder: 1,
questionsCount: QUIZ_METADATA.questionsCount,
timeLimitSeconds: QUIZ_METADATA.timeLimitSeconds,
isActive: true,
}).where(eq(quizzes.id, existing.id));

const quizId = existing.id;
// Insert translations
for (const locale of LOCALES) {
await db.insert(quizTranslations).values({
quizId,
locale,
title: QUIZ_METADATA.translations[locale].title,
description: QUIZ_METADATA.translations[locale].description,
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Wrap delete/update operations in a transaction.

The sequence of deletes (lines 95-96), update (lines 97-104), and inserts (lines 108-115) is not wrapped in a transaction. If any operation fails midway, the database could be left in an inconsistent state with partial data deleted/updated.

🔎 Proposed transaction wrapper
     if (existingAttempt) {
       throw new Error(`Quiz ${QUIZ_METADATA.slug} has existing attempts. Aborting to avoid data loss.`);
     }
 
-    await db.delete(quizQuestions).where(eq(quizQuestions.quizId, existing.id));
-    await db.delete(quizTranslations).where(eq(quizTranslations.quizId, existing.id));
-    await db.update(quizzes).set({
-      topicId: existing.topicId,
-      slug: QUIZ_METADATA.slug,
-      displayOrder: 1,
-      questionsCount: QUIZ_METADATA.questionsCount,
-      timeLimitSeconds: QUIZ_METADATA.timeLimitSeconds,
-      isActive: true,
-    }).where(eq(quizzes.id, existing.id));
-
-    const quizId = existing.id;
-    // Insert translations
-    for (const locale of LOCALES) {
-      await db.insert(quizTranslations).values({
-        quizId,
-        locale,
-        title: QUIZ_METADATA.translations[locale].title,
-        description: QUIZ_METADATA.translations[locale].description,
-      });
-    }
+    await db.transaction(async (tx) => {
+      await tx.delete(quizQuestions).where(eq(quizQuestions.quizId, existing.id));
+      await tx.delete(quizTranslations).where(eq(quizTranslations.quizId, existing.id));
+      await tx.update(quizzes).set({
+        topicId: existing.topicId,
+        slug: QUIZ_METADATA.slug,
+        displayOrder: 1,
+        questionsCount: QUIZ_METADATA.questionsCount,
+        timeLimitSeconds: QUIZ_METADATA.timeLimitSeconds,
+        isActive: true,
+      }).where(eq(quizzes.id, existing.id));
+
+      // Insert translations
+      for (const locale of LOCALES) {
+        await tx.insert(quizTranslations).values({
+          quizId: existing.id,
+          locale,
+          title: QUIZ_METADATA.translations[locale].title,
+          description: QUIZ_METADATA.translations[locale].description,
+        });
+      }
+    });
+
+    const quizId = existing.id;

Apply similar transaction wrapping to the new quiz creation path (lines 121-138).

🤖 Prompt for AI Agents
In frontend/db/seed-quiz-javascript.ts around lines 95 to 115, the deletes,
update, and subsequent inserts must be executed inside a single database
transaction to avoid leaving the DB in a partial state if an operation fails;
wrap the delete(quizQuestions), delete(quizTranslations), update(quizzes) and
the loop that inserts quizTranslations in a single transaction using the
project's DB transaction API (use the transaction client for all queries inside
the callback and ensure errors propagate so the transaction rolls back), and
apply the same transaction wrapper to the new-quiz creation path around lines
121-138 so both update-and-reinsert and create flows are atomic.

Comment on lines +143 to +184
async function seedQuestions(questions: QuestionData[], quizId: string, partNumber: number) {
console.log(`Seeding ${questions.length} questions from part ${partNumber}...`);

for (const question of questions) {
// DB generates question id
const [q] = await db.insert(quizQuestions).values({
quizId,
displayOrder: question.order,
difficulty: question.difficulty,
}).returning();

for (const locale of LOCALES) {
await db.insert(quizQuestionContent).values({
quizQuestionId: q.id,
locale,
questionText: question[locale].q,
explanation: createExplanation(question[locale].exp),
});
}

for (let i = 0; i < question.answers.length; i++) {
const answer = question.answers[i];

// DB generates answer id
const [a] = await db.insert(quizAnswers).values({
quizQuestionId: q.id,
displayOrder: i + 1,
isCorrect: answer.correct,
}).returning();

for (const locale of LOCALES) {
await db.insert(quizAnswerTranslations).values({
quizAnswerId: a.id,
locale,
answerText: answer[locale],
});
}
}
}

console.log(`Part ${partNumber} completed (${questions.length} questions)`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Wrap question seeding in a transaction.

Similar to ensureQuizExists, this function performs multiple sequential database operations without transaction wrapping. If an insert fails partway through, the database could be left with incomplete question data (e.g., questions without answers or missing translations).

🔎 Proposed transaction wrapper
 async function seedQuestions(questions: QuestionData[], quizId: string, partNumber: number) {
   console.log(`Seeding ${questions.length} questions from part ${partNumber}...`);
 
   for (const question of questions) {
-    // DB generates question id
-    const [q] = await db.insert(quizQuestions).values({
-      quizId,
-      displayOrder: question.order,
-      difficulty: question.difficulty,
-    }).returning();
-
-    for (const locale of LOCALES) {
-      await db.insert(quizQuestionContent).values({
-        quizQuestionId: q.id,
-        locale,
-        questionText: question[locale].q,
-        explanation: createExplanation(question[locale].exp),
-      });
-    }
-
-    for (let i = 0; i < question.answers.length; i++) {
-      const answer = question.answers[i];
-      
-      // DB generates answer id
-      const [a] = await db.insert(quizAnswers).values({
-        quizQuestionId: q.id,
-        displayOrder: i + 1,
-        isCorrect: answer.correct,
-      }).returning();
-
-      for (const locale of LOCALES) {
-        await db.insert(quizAnswerTranslations).values({
-          quizAnswerId: a.id,
-          locale,
-          answerText: answer[locale],
-        });
+    await db.transaction(async (tx) => {
+      const [q] = await tx.insert(quizQuestions).values({
+        quizId,
+        displayOrder: question.order,
+        difficulty: question.difficulty,
+      }).returning();
+
+      for (const locale of LOCALES) {
+        await tx.insert(quizQuestionContent).values({
+          quizQuestionId: q.id,
+          locale,
+          questionText: question[locale].q,
+          explanation: createExplanation(question[locale].exp),
+        });
       }
-    }
+
+      for (let i = 0; i < question.answers.length; i++) {
+        const answer = question.answers[i];
+        
+        const [a] = await tx.insert(quizAnswers).values({
+          quizQuestionId: q.id,
+          displayOrder: i + 1,
+          isCorrect: answer.correct,
+        }).returning();
+
+        for (const locale of LOCALES) {
+          await tx.insert(quizAnswerTranslations).values({
+            quizAnswerId: a.id,
+            locale,
+            answerText: answer[locale],
+          });
+        }
+      }
+    });
   }
 
   console.log(`Part ${partNumber} completed (${questions.length} questions)`);
🤖 Prompt for AI Agents
In frontend/db/seed-quiz-javascript.ts around lines 143 to 184, wrap the
multi-step seeding logic in a single database transaction so partial failures
don't leave orphaned rows; call db.transaction(async (tx) => { ... }) (or the
project's equivalent) and perform all inserts inside using the transaction
handle (replace db.insert/... with tx.insert/...), ensuring you use
tx.returning() where needed; let the transaction auto-commit on success and
rollback on any thrown error so each question (and its translations/answers) is
atomic.

@LesiaUKR LesiaUKR closed this Dec 21, 2025
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.

1 participant