Skip to content

feat(quiz): redesign quiz cards with categories and countdown timer#89

Merged
ViktorSvertoka merged 3 commits into
developfrom
sl/feat/quiz
Dec 30, 2025
Merged

feat(quiz): redesign quiz cards with categories and countdown timer#89
ViktorSvertoka merged 3 commits into
developfrom
sl/feat/quiz

Conversation

@LesiaUKR
Copy link
Copy Markdown
Collaborator

@LesiaUKR LesiaUKR commented Dec 29, 2025

Quiz Cards Redesign:

  • Add grid layout (2 columns desktop, 1 mobile) for better visual hierarchy

  • Implement category tabs with adaptive breakpoints (3/5/10 columns)

  • Create Badge component for reusable status indicators

  • Add QuizCard component with category badge and completion status

  • Display user progress (best score, attempts, progress bar) for authenticated users

  • Show conditional CTA ("Retake Quiz" vs "Start Quiz")

    Countdown Timer:

  • Create CountdownTimer component with MM:SS format display

  • Implement color-coded warnings (blue/yellow/red based on remaining time)

  • Add auto-submit functionality when timer expires

  • Support dynamic time calculation (DB value or 30 sec/question fallback)

  • Integrate pulse animation for critical time warnings (< 10%)

    Database & Queries:

  • Update getActiveQuizzes() to join categories and translations

  • Add getUserQuizzesProgress() with Map-based O(1) lookup optimization

  • Support timeLimitSeconds as nullable with fallback logic

Summary by CodeRabbit

  • New Features

    • Countdown timer with color-coded warnings, progress bar and automatic submit on time-up
    • Per-user quiz progress (best score, completion %, attempts) and session-aware quiz listing
    • Category-based tabbed quiz browsing and updated quiz cards showing progress and estimated time
    • Visual badge component for category/status
  • Bug Fixes

    • Time always shown using a reliable fallback for estimated duration
    • Robust saved-quiz banner parsing and improved pending-result submission error handling
    • Auth check added to guest result submission API

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

  Quiz Cards Redesign:
- Add grid layout (2 columns desktop, 1 mobile) for better visual hierarchy
- Implement category tabs with adaptive breakpoints (3/5/10 columns)
- Create Badge component for reusable status indicators
- Add QuizCard component with category badge and completion status
- Display user progress (best score, attempts, progress bar) for authenticated users
- Show conditional CTA ("Retake Quiz" vs "Start Quiz")

  Countdown Timer:
- Create CountdownTimer component with MM:SS format display
- Implement color-coded warnings (blue/yellow/red based on remaining time)
- Add auto-submit functionality when timer expires
- Support dynamic time calculation (DB value or 30 sec/question fallback)
- Integrate pulse animation for critical time warnings (< 10%)

  Database & Queries:
- Update getActiveQuizzes() to join categories and translations
- Add getUserQuizzesProgress() with Map-based O(1) lookup optimization
- Support timeLimitSeconds as nullable with fallback logic
@netlify
Copy link
Copy Markdown

netlify Bot commented Dec 29, 2025

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit b65d32d
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/6952eca1b0222a0008f43533
😎 Deploy Preview https://deploy-preview-89--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 29, 2025

📝 Walkthrough

Walkthrough

Adds per-user quiz progress data and category fields, new UI components (QuizCard, QuizzesSection, Badge, CountdownTimer), integrates a client-side countdown into QuizContainer with timeLimitSeconds passed consistently, and hardens guest-result and pending-result handling.

Changes

Cohort / File(s) Summary
Quiz pages
frontend/app/[locale]/quiz/[slug]/page.tsx, frontend/app/[locale]/quizzes/page.tsx
Always compute and pass timeLimitSeconds to QuizContainer; quizzes page now fetches current user and per-user progress and delegates rendering to QuizzesSection.
Countdown / Timer integration
frontend/components/quiz/CountdownTimer.tsx, frontend/components/quiz/QuizContainer.tsx
New CountdownTimer component (props: timeLimitSeconds, onTimeUp, isActive); QuizContainer adds timeLimitSeconds prop, wires the timer and calls submit on time-up.
Quiz list / card UI
frontend/components/quiz/QuizCard.tsx, frontend/components/quiz/QuizzesSection.tsx
New QuizCard (renders metadata and optional user progress) and QuizzesSection (tabbed category view rendering QuizCard grid with user progress).
UI primitive
frontend/components/ui/badge.tsx
New Badge forwardRef component with variants (default, success, blue, purple, gray).
DB & progress queries
frontend/db/queries/quiz.ts
Quiz type extended with categorySlug and categoryName; added UserQuizProgress interface and getUserQuizzesProgress(userId) to aggregate per-quiz stats.
API auth check
frontend/app/api/quiz/guest-result/route.ts
Added getCurrentUser check in POST to require session and ensure session.id === body.userId, returning 401 on mismatch.
Client robustness fixes
frontend/components/dashboard/QuizSavedBanner.tsx, frontend/components/quiz/PendingResultHandler.tsx
Safer JSON parsing/removal of quiz_just_saved; pending-result handler now inspects res.ok before clearing and logs API errors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant QuizzesPage as /quizzes Page
    participant Auth as getCurrentUser()
    participant QDB as getActiveQuizzes() / getUserQuizzesProgress()
    participant UI as QuizzesSection / QuizCard

    User->>QuizzesPage: Request /quizzes
    QuizzesPage->>Auth: getCurrentUser()
    Auth-->>QuizzesPage: session|null
    QuizzesPage->>QDB: getActiveQuizzes()
    QDB-->>QuizzesPage: quizzes list
    alt session present
        QuizzesPage->>QDB: getUserQuizzesProgress(userId)
        QDB-->>QuizzesPage: progress map
    end
    QuizzesPage->>UI: Render QuizzesSection(quizzes, userProgressMap)
    UI-->>User: Tabbed quiz grid with QuizCards (progress badges)
Loading
sequenceDiagram
    participant User
    participant QuizPage as /quiz/[slug] Page
    participant Container as QuizContainer
    participant Timer as CountdownTimer
    participant Submit as submit handler

    User->>QuizPage: Load quiz page
    QuizPage->>Container: Render with timeLimitSeconds
    User->>Container: Start quiz
    Container->>Timer: isActive = true, start interval
    loop every 1s
        Timer->>Timer: decrement remaining
        Timer-->>User: update UI (progress, color)
    end
    Timer->>Container: onTimeUp()
    Container->>Submit: handleSubmit()
    Submit-->>User: submit result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐇
A tiny rabbit counts the ticks,
Cards and badges, helpful tricks.
Progress hops from row to row,
When the timer ends—off we go! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main features: quiz card redesign with categories and countdown timer implementation.
✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (9)
frontend/components/ui/badge.tsx (1)

15-24: The 'gray' and 'default' variants have identical styling.

Lines 15-16 and 23-24 apply the same background and text colors in both light and dark modes. This duplication may cause confusion and makes the 'gray' variant redundant.

💡 Consider removing the duplicate variant or differentiating the styles

Option 1: Remove the redundant 'gray' variant

 export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
-  variant?: 'default' | 'success' | 'blue' | 'purple' | 'gray';
+  variant?: 'default' | 'success' | 'blue' | 'purple';
 }

And remove the corresponding styling block:

           variant === 'purple' &&
             'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400',
-          variant === 'gray' &&
-            'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400',
           
           className

Option 2: Differentiate the styling

If both variants are needed, ensure they have distinct visual appearances.

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

345-361: Consider validating totalQuestions consistency across attempts.

The aggregation correctly tracks bestScore and attemptsCount, but totalQuestions is taken from the first attempt encountered (line 352) and never updated. If the quiz structure changed between attempts, totalQuestions could theoretically differ across attempts for the same quiz.

While this scenario is unlikely in practice, consider whether validation or consistency checks are needed.

💡 Optional: Add consistency validation

If totalQuestions should be immutable per quiz, you could add a consistency check:

     } else {
       existing.attemptsCount += 1;
+      // Validate consistency (optional)
+      if (row.totalQuestions !== existing.totalQuestions) {
+        console.warn(`Quiz ${row.quizId}: totalQuestions mismatch`);
+      }
       if (row.score > existing.bestScore) {
         existing.bestScore = row.score;
       }
     }

Alternatively, use the maximum totalQuestions seen:

       if (row.score > existing.bestScore) {
         existing.bestScore = row.score;
+        existing.totalQuestions = row.totalQuestions;
       }
frontend/app/[locale]/quizzes/page.tsx (1)

15-20: Replace any type with proper UserQuizProgress type.

Line 15 uses Record<string, any> for userProgressMap, which loses type safety. The actual structure is UserQuizProgress from the database queries.

🔎 Proposed fix for type safety

Import the type and use it:

-import { getActiveQuizzes, getUserQuizzesProgress } from '@/db/queries/quiz';
+import { 
+  getActiveQuizzes, 
+  getUserQuizzesProgress,
+  type UserQuizProgress 
+} from '@/db/queries/quiz';
 import { getCurrentUser } from '@/lib/auth';
 import QuizzesSection from '@/components/quiz/QuizzesSection';

Then update the type:

-  let userProgressMap: Record<string, any> = {};
+  let userProgressMap: Record<string, UserQuizProgress> = {};
frontend/components/quiz/QuizzesSection.tsx (1)

9-24: Avoid duplicating type definitions; import from the source.

Lines 9-18 duplicate the Quiz interface from frontend/db/queries/quiz.ts, and lines 20-24 define a UserProgress interface that closely resembles UserQuizProgress but omits lastAttemptAt. This duplication can lead to inconsistencies and maintenance issues.

🔎 Proposed fix

Import the types from the source:

 'use client';
 
 import { useState } from 'react';
 import { useParams } from 'next/navigation';
 import { QuizCard } from './QuizCard';
 import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
 import { categoryData } from '@/data/category';
+import type { Quiz, UserQuizProgress } from '@/db/queries/quiz';
 
-interface Quiz {
-  id: string;
-  slug: string;
-  title: string | null;
-  description: string | null;
-  questionsCount: number;
-  timeLimitSeconds: number | null;
-  categorySlug: string | null;
-  categoryName: string | null;
-}
-
-interface UserProgress {
-  bestScore: number;
-  totalQuestions: number;
-  attemptsCount: number;
-}
-
 interface QuizzesSectionProps {
   quizzes: Quiz[];
-  userProgressMap: Record<string, UserProgress>;
+  userProgressMap: Record<string, UserQuizProgress>;
 }

If lastAttemptAt is not needed in the UI, you can create a derived type using Omit:

type UserProgress = Omit<UserQuizProgress, 'lastAttemptAt'>;
frontend/components/quiz/CountdownTimer.tsx (2)

19-34: Add onTimeUp to useCallback dependencies or use a ref to prevent interval recreation.

The useEffect at line 34 includes onTimeUp in its dependency array. If the parent component doesn't memoize this callback with useCallback, the effect will re-run on every render, potentially creating multiple intervals (though the cleanup prevents overlapping timers, it's still inefficient).

💡 Recommended approaches

Option 1: Use a ref for the callback (recommended)

 'use client';
 
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
 import { cn } from '@/lib/utils';
 export function CountdownTimer({
   timeLimitSeconds,
   onTimeUp,
   isActive,
 }: CountdownTimerProps) {
   const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
+  const onTimeUpRef = useRef(onTimeUp);
+  
+  useEffect(() => {
+    onTimeUpRef.current = onTimeUp;
+  }, [onTimeUp]);
 
   useEffect(() => {
     if (!isActive) return;
 
     const interval = setInterval(() => {
       setRemainingSeconds(prev => {
         if (prev <= 1) {
           clearInterval(interval);
-          onTimeUp();
+          onTimeUpRef.current();
           return 0;
         }
         return prev - 1;
       });
     }, 1000);
 
     return () => clearInterval(interval);
-  }, [isActive, onTimeUp]);
+  }, [isActive]);

Option 2: Document that parent must memoize the callback

Add a comment noting that onTimeUp should be wrapped in useCallback in the parent component.


65-83: Consider internationalizing hard-coded Ukrainian text.

Lines 65 ("Залишилось часу:"), 83 ("⚠️ Час майже закінчився!", "⏰ Поспішайте!") contain hard-coded Ukrainian strings. Since the app supports multiple locales (as seen in other components), consider using i18n for consistency.

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

24-26: Add guard against division by zero in percentage calculation.

If userProgress.totalQuestions is 0, the division at line 25 will produce NaN or Infinity. While unlikely in production, this edge case should be handled defensively.

🔎 Proposed fix
   const percentage = userProgress
-    ? Math.round((userProgress.bestScore / userProgress.totalQuestions) * 100)
+    ? userProgress.totalQuestions > 0
+      ? Math.round((userProgress.bestScore / userProgress.totalQuestions) * 100)
+      : 0
     : 0;

67-67: Consider internationalizing the hard-coded English text.

Line 67 uses hard-coded English strings 'attempt' and 'attempts'. Since the app supports multiple locales, consider using i18n for consistency with the rest of the application.

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

309-318: Simplify by computing calculatedTime outside the JSX.

Lines 309-318 use an immediately invoked function expression (IIFE) to compute calculatedTime within the JSX. This adds unnecessary complexity and makes the code harder to read.

🔎 Proposed refactor

Compute the value before the return statement:

+  const calculatedTime = timeLimitSeconds ?? (totalQuestions * 30);
+
   return (
     <div className="space-y-8 no-select">
       <QuizProgress
         current={state.currentIndex}
         total={totalQuestions}
         answers={state.answers}
       />
 
-      {(() => {
-        const calculatedTime = timeLimitSeconds ?? (totalQuestions * 30);
-        return (
-          <CountdownTimer
-            timeLimitSeconds={calculatedTime}
-            onTimeUp={handleTimeUp}
-            isActive={state.status === 'in_progress'}
-          />
-        );
-      })()}
+      <CountdownTimer
+        timeLimitSeconds={calculatedTime}
+        onTimeUp={handleTimeUp}
+        isActive={state.status === 'in_progress'}
+      />
 
       <QuizQuestion
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26101cd and ffc59b6.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • frontend/app/[locale]/quiz/[slug]/page.tsx
  • frontend/app/[locale]/quizzes/page.tsx
  • frontend/components/quiz/CountdownTimer.tsx
  • frontend/components/quiz/QuizCard.tsx
  • frontend/components/quiz/QuizContainer.tsx
  • frontend/components/quiz/QuizzesSection.tsx
  • frontend/components/ui/badge.tsx
  • frontend/db/queries/quiz.ts
🧰 Additional context used
🧬 Code graph analysis (5)
frontend/components/quiz/CountdownTimer.tsx (1)
frontend/lib/utils.ts (1)
  • cn (4-6)
frontend/app/[locale]/quizzes/page.tsx (4)
frontend/app/[locale]/layout.tsx (1)
  • dynamic (17-17)
frontend/lib/auth.ts (1)
  • getCurrentUser (94-115)
frontend/db/queries/quiz.ts (2)
  • getActiveQuizzes (106-141)
  • getUserQuizzesProgress (329-365)
frontend/components/quiz/QuizzesSection.tsx (1)
  • QuizzesSection (31-105)
frontend/components/quiz/QuizzesSection.tsx (4)
frontend/db/queries/quiz.ts (1)
  • Quiz (15-25)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/components/ui/tabs.tsx (4)
  • Tabs (66-66)
  • TabsList (66-66)
  • TabsTrigger (66-66)
  • TabsContent (66-66)
frontend/components/quiz/QuizCard.tsx (1)
  • QuizCard (23-79)
frontend/components/ui/badge.tsx (1)
frontend/lib/utils.ts (1)
  • cn (4-6)
frontend/db/queries/quiz.ts (3)
frontend/db/schema/categories.ts (2)
  • categories (12-19)
  • categoryTranslations (21-33)
frontend/db/schema/quiz.ts (2)
  • quizzes (19-41)
  • quizAttempts (128-162)
frontend/db/index.ts (1)
  • db (17-17)
🔇 Additional comments (1)
frontend/db/queries/quiz.ts (1)

137-138: LGTM! Ordering includes category display order.

The ordering by categories.displayOrder followed by quizzes.displayOrder provides a clear hierarchical sort. Since quizzes.categoryId is marked as notNull in the schema (as shown in the relevant snippets), the LEFT JOIN to categories will always succeed, ensuring displayOrder is never NULL.

Comment thread frontend/app/[locale]/quiz/[slug]/page.tsx Outdated
Comment thread frontend/components/quiz/QuizCard.tsx Outdated
Comment thread frontend/components/quiz/QuizzesSection.tsx Outdated
- Validate session userId matches request userId (security)
- Check res.ok before clearing localStorage (data loss prevention)
- Remove diff markers and fix formatting

Security: Prevents attackers from saving quiz results to other users' accounts
Bug fix: Guest quiz data no longer lost on server errors
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: 1

🧹 Nitpick comments (6)
frontend/components/dashboard/QuizSavedBanner.tsx (1)

20-27: Good error handling for sessionStorage parsing.

The try-catch block correctly prevents crashes from malformed JSON and ensures cleanup happens in both success and failure paths.

Consider adding runtime validation to ensure the parsed data matches the SavedQuizInfo interface before calling setInfo. This would prevent potential runtime errors if the stored data has an unexpected shape (e.g., missing fields).

Optional: Add type guard for extra safety
+ function isSavedQuizInfo(data: any): data is SavedQuizInfo {
+   return (
+     typeof data === 'object' &&
+     data !== null &&
+     typeof data.score === 'number' &&
+     typeof data.total === 'number' &&
+     typeof data.percentage === 'number' &&
+     typeof data.pointsAwarded === 'number' &&
+     typeof data.quizSlug === 'string'
+   );
+ }
+
  useEffect(() => {
    const saved = sessionStorage.getItem('quiz_just_saved');
    if (saved) {
       try {
-       setInfo(JSON.parse(saved));
+       const parsed = JSON.parse(saved);
+       if (isSavedQuizInfo(parsed)) {
+         setInfo(parsed);
+       } else {
+         console.error('Invalid quiz_just_saved data shape:', parsed);
+       }
        sessionStorage.removeItem('quiz_just_saved');
     } catch (error) {
       console.error('Failed to parse quiz_just_saved from sessionStorage:', error);
      sessionStorage.removeItem('quiz_just_saved');
    }
    }
  }, []);
frontend/components/quiz/PendingResultHandler.tsx (2)

15-26: Remove redundant condition check.

Line 13 already returns early if pending is falsy, making the if (pending) check on line 15 redundant. You can safely remove the outer condition.

🔎 Proposed simplification
-    if (pending) {
-      fetch("/api/quiz/guest-result", {
+    fetch("/api/quiz/guest-result", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({
           userId,
           quizId: pending.quizId,
           answers: pending.answers,
           violations: pending.violations,
           timeSpentSeconds: pending.timeSpentSeconds,
         }),
       })
-      .then(async (res) => {
+    .then(async (res) => {
         if (res.ok) {
           clearPendingQuizResult();
         } else {
           console.error("Guest-result API error:", res.status);
         }
       })
-        .catch(err => {
+      .catch(err => {
           console.error("Guest-result fetch error:", err);
         });
-    }

27-33: Good defensive error handling; consider minor refinements.

The res.ok check correctly prevents clearing pending results on API errors, which addresses the PR objective to prevent guest quiz data loss. This is a solid improvement.

Optional refinements:

  1. Remove unnecessary async keyword (line 27): The handler doesn't use await, so the async modifier is redundant.

  2. Enhance error logging: Consider logging more context (e.g., response body) to aid debugging:

    .then(async (res) => {
      if (res.ok) {
        clearPendingQuizResult();
      } else {
        const errorText = await res.text();
        console.error("Guest-result API error:", res.status, errorText);
      }
    })

Broader consideration: Currently, if the API call fails, the pending result remains in localStorage indefinitely with no automatic retry. You might consider implementing a retry mechanism (e.g., exponential backoff on component mount) or a background sync strategy to ensure guest results are eventually persisted.

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

9-18: Interface inconsistency: missing isActive field.

The local Quiz interface omits the isActive field present in the canonical type from frontend/db/queries/quiz.ts. While this works because TypeScript structural typing allows wider types to be passed to narrower interfaces, it creates maintenance risk if isActive is later needed here.

🔎 Consider importing the canonical type
-interface Quiz {
-  id: string;
-  slug: string;
-  title: string | null;
-  description: string | null;
-  questionsCount: number;
-  timeLimitSeconds: number | null;
-  categorySlug: string | null;
-  categoryName: string | null;
-}
+import type { Quiz } from '@/db/queries/quiz';

Or add the missing field:

 interface Quiz {
   id: string;
   slug: string;
   title: string | null;
   description: string | null;
   questionsCount: number;
   timeLimitSeconds: number | null;
+  isActive: boolean;
   categorySlug: string | null;
   categoryName: string | null;
 }
frontend/components/quiz/QuizCard.tsx (2)

31-31: Hardcoded 'Uncategorized' string may need internationalization.

The fallback string 'Uncategorized' is not localized. If the app supports multiple languages (as evidenced by the locale handling in QuizzesSection.tsx), this should use a translation key or the component should accept a locale prop.


71-76: Conditional CTA text could benefit from internationalization.

The button text ("Retake Quiz" / "Start Quiz") is hardcoded. For consistency with the multilingual support evident in the codebase, consider using translation keys.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffc59b6 and fa42ba4.

📒 Files selected for processing (6)
  • frontend/app/[locale]/quiz/[slug]/page.tsx
  • frontend/app/api/quiz/guest-result/route.ts
  • frontend/components/dashboard/QuizSavedBanner.tsx
  • frontend/components/quiz/PendingResultHandler.tsx
  • frontend/components/quiz/QuizCard.tsx
  • frontend/components/quiz/QuizzesSection.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/[locale]/quiz/[slug]/page.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
frontend/app/api/quiz/guest-result/route.ts (1)
frontend/lib/auth.ts (1)
  • getCurrentUser (94-115)
frontend/components/quiz/QuizzesSection.tsx (2)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/components/quiz/QuizCard.tsx (1)
  • QuizCard (23-79)
frontend/components/quiz/QuizCard.tsx (2)
frontend/components/ui/badge.tsx (1)
  • Badge (36-36)
frontend/db/seed-quiz-types.ts (1)
  • p (37-40)
frontend/components/quiz/PendingResultHandler.tsx (1)
frontend/lib/guest-quiz.ts (1)
  • clearPendingQuizResult (51-54)
🔇 Additional comments (5)
frontend/app/api/quiz/guest-result/route.ts (1)

30-36: LGTM: Authentication check prevents privilege escalation.

The session validation correctly ensures users can only save quiz results for their own account, preventing a potential privilege escalation where a user could submit results under another user's ID.

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

37-41: Locale validation logic is correct.

The type guard ensures only valid locales are used with a safe fallback to 'en'.


63-97: Category filtering and rendering looks good.

The component correctly filters quizzes per category and provides a friendly empty state. The fallback at line 82 (quiz.categoryName ?? category.slug) appropriately handles null category names.

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

47-47: Time calculation fallback aligns with PR requirements.

The fallback formula quiz.questionsCount * 30 (30 seconds per question) matches the PR description's "Dynamic time calculation: use DB value or fallback to 30 seconds per question."


50-69: Progress visualization is well-implemented.

The progress section clearly displays best score, completion percentage, and attempt count. The progress bar provides good visual feedback.

Comment thread frontend/components/quiz/QuizCard.tsx
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: 0

🧹 Nitpick comments (3)
frontend/components/quiz/QuizCard.tsx (3)

1-1: Consider removing 'use client' directive if not needed.

This component doesn't use any client-side hooks, state, or event handlers. It could potentially be a server component, which would improve performance. However, this directive might be intentional for future enhancements or required by parent component constraints.


60-65: Consider adding ARIA labels for accessibility.

The progress bar would be more accessible with ARIA attributes to help screen readers communicate the completion status to users.

🔎 Suggested enhancement
-          <div className="h-1.5 bg-gray-200 dark:bg-neutral-800 rounded-full overflow-hidden">
+          <div 
+            className="h-1.5 bg-gray-200 dark:bg-neutral-800 rounded-full overflow-hidden"
+            role="progressbar"
+            aria-valuenow={percentage}
+            aria-valuemin={0}
+            aria-valuemax={100}
+            aria-label={`Quiz completion: ${percentage}%`}
+          >
             <div
               className="h-full bg-blue-600 rounded-full transition-all"
               style={{ width: `${percentage}%` }}
             />
           </div>

45-48: Optional: Replace emoji icons with SVG for better accessibility.

While emoji icons (📝, ⏱️) are visually appealing, they may not be announced consistently by all screen readers. Consider using SVG icons with proper aria-label attributes or text alternatives for improved accessibility.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa42ba4 and b65d32d.

📒 Files selected for processing (1)
  • frontend/components/quiz/QuizCard.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/components/quiz/QuizCard.tsx (2)
frontend/components/ui/badge.tsx (1)
  • Badge (36-36)
frontend/db/seed-quiz-types.ts (1)
  • p (37-40)
🔇 Additional comments (3)
frontend/components/quiz/QuizCard.tsx (3)

6-21: Well-structured type definitions.

The interface properly defines all props with correct nullable types. The categoryName: string | null on line 14 correctly matches the source type and the component handles the null case with a fallback (line 31). Excellent work addressing the previous review feedback.


23-26: Excellent defensive coding for percentage calculation.

The calculation now properly guards against division by zero by checking userProgress.totalQuestions > 0 before performing the division. This addresses the previous review concern and ensures robust handling of edge cases.


28-78: Clean implementation with good defensive coding.

The component properly handles all nullable fields, provides appropriate fallbacks, and includes conditional rendering for user progress. The time calculation (line 47) correctly uses a fallback of 30 seconds per question when timeLimitSeconds is null, and the CTA text adapts based on whether the user has attempted the quiz.

Copy link
Copy Markdown
Member

@ViktorSvertoka ViktorSvertoka left a comment

Choose a reason for hiding this comment

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

Good job!

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.

2 participants