feat(quiz): guest results persistence, uuid migration, and quizzes page#69
feat(quiz): guest results persistence, uuid migration, and quizzes page#69LesiaUKR wants to merge 8 commits into
Conversation
- 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
WalkthroughAdds 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
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
/quizand/leaderboardwhile keeping/dashboardprotected 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.hrefcauses 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.hrefis 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-resultwith the same payload structure (userId,quizId,answers,violations,timeSpentSeconds) is duplicated inlogin/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
localefor fetching localized quiz data but renders UI text in English (lines 17-18, 28-34, 55, 67). Consider usingnext-intltranslations 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.
percentageis formatted as a string withtoFixed(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 sharedLocaletype.The
Localetype is already defined inseed-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: UseLinkcomponent orrouter.pushinstead ofwindow.location.href.Using
window.location.hreftriggers a full page reload, losing React state and client-side navigation benefits. TheLinkcomponent from@/i18n/routingis already used elsewhere in the codebase for locale-aware navigation.Additionally,
quizSlugshould 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.hreftriggers a full page reload. Consider using Next.js router (useRouterfromnext/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.tsbut the file is located atfrontend/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
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis 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.claudeto gitignore.Adding
.claudeto 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), whilejavascript-quiz-part3.jsonuses"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-fundamentalsto/quizzesproperly 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
userIdenables 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
revalidatePathwith 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 showrevalidatePath('/', 'layout'). The code on line 69 callsrevalidatePath('/[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:
quizSlugprop enables result tracking- Nullable
userIdsupports guest usersPendingResultHandlerconditionally submits pending results when user is presentfrontend/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
Linkcomponent 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
getVisibleIndiceshelper 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 ?? nullanduser.createdAt ?? nullare 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
questionsCountfield cannot be null, so no null check is needed there. ThetimeLimitSecondsfield 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
userIdnullable and derivingisGuestfrom it. ThequizSlugaddition 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
QuizResultfor 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. TheQuizQuestionSeedinterface 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 constassertion on'listItem'ensures proper type narrowing.
| timeSpentSeconds: pendingResult.timeSpentSeconds, | ||
| }), | ||
| }); | ||
| const result = await res.json(); |
There was a problem hiding this comment.
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.
| 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.
| window.location.href = `/${locale}/dashboard`; | ||
| return; |
There was a problem hiding this comment.
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.
| 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).
| interface QuestionData { | ||
| id: string; | ||
| order: number; | ||
| difficulty: 'beginner' | 'medium' | 'advanced'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find schema files and seed files
fd -t f -e ts -e prisma | grep -E "(schema|seed)" | head -20Repository: 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 -30Repository: 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 2Repository: 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 -100Repository: 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.
| const part1Path = join(__dirname, '../data/react-quiz-data-part1.json'); | ||
| const part2Path = join(__dirname, '../data/react-quiz-data-part2.json'); |
There was a problem hiding this comment.
🧩 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"
fiRepository: 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.tsRepository: 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.jsonRepository: 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.
| if (existing) { | ||
| await db.delete(quizzes).where(eq(quizzes.id, existing.id)); | ||
| console.log('Old quiz deleted'); | ||
| } |
There was a problem hiding this comment.
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.
| // 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(); |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -10Repository: 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.tsRepository: DevLoversTeam/devlovers.net
Length of output: 9779
🏁 Script executed:
#!/bin/bash
# Check the main drizzle schema
cat -n frontend/drizzle/schema.ts | head -100Repository: 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 -30Repository: 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=tsRepository: 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*.tsRepository: DevLoversTeam/devlovers.net
Length of output: 717
🏁 Script executed:
#!/bin/bash
# Check the main seed.ts file
cat -n frontend/db/seed.tsRepository: 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 -10Repository: 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 -15Repository: 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.
There was a problem hiding this comment.
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
topicIdvalues that don't reference any existing topic in the database, resulting in orphaned records without valid topic references.Either create and seed a
topicstable with proper foreign key constraints before inserting quizzes, or maketopicIdnullable 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 DBAlternatively, 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
readFileSynccall 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
existsSyncfrom 'fs'.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 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.idis 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: stringuserId: string | nullThese 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.
| 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, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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)`); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
Quiz flow (guest -> auth)
Middleware (i18n + auth)
Database & schema changes
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.