Skip to content

feat: implement quiz detail screen and crud - #11

Merged
Truella merged 19 commits into
masterfrom
feat/quiz-detail-crud
Jul 28, 2026
Merged

feat: implement quiz detail screen and crud#11
Truella merged 19 commits into
masterfrom
feat/quiz-detail-crud

Conversation

@Truella

@Truella Truella commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary
Quiz detail page at /dashboard/quiz/[quizId] with three tabs: Questions (edit/delete each question inline), Settings (edit metadata, visibility, time limit, category/difficulty, delete quiz with confirm), and Stats (attempt count, avg score, avg time, recent attempts list). useQuizDetail hook handling all data fetching and mutations. CSV question preview before publish on the create page. "View" button on QuizCard replaced with "Manage" linking to the detail page.

New files

  • src/hooks/useQuizDetail.ts
  • src/components/quiz-detail/MetaEditor.tsx
  • src/components/quiz-detail/QuestionEditor.tsx
  • src/components/quiz-detail/AttemptStats.tsx
  • src/views/QuizDetailView.tsx
  • app/dashboard/quiz/[quizId]/page.tsx

Changed files

  • src/components/QuizCard.tsx
  • src/views/CreateQuiz.tsx

Checklist

  • npx tsc --noEmit passes
  • npx next lint passes
  • Quiz detail page loads with all three tabs working
  • Editing question title/options saves to Supabase on "Save question"
  • Deleting a question requires confirm then removes from list
  • Editing metadata saves on blur/change
  • Delete quiz navigates back to My Quizzes
  • CSV preview shows parsed questions with amber correct answer highlight
  • Manage button on QuizCard navigates to detail page
  • Light mode works correctly on detail page
  • No console.log statements
  • Branch up to date with main

Summary by CodeRabbit

  • New Features
    • Added a quiz detail page with Questions, Settings, and Stats tabs.
    • Enabled editing/saving/deleting for quiz metadata and individual questions, including a two-step delete flow.
    • Added attempt statistics with recent attempts plus average score and completion time.
    • Added share-link copying and an in-CSV preview of parsed questions.
  • Improvements
    • Updated quiz cards to show “times taken” and switch to Manage links; improved quiz bank preview to consistently display attempt counts.
  • Tests
    • Updated quiz bank hook tests and enhanced test chain utilities.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
prep Ready Ready Preview, Comment Jul 28, 2026 11:37am

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Truella, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ccec14eb-343a-405f-ba6f-fcf5c9c2887f

📥 Commits

Reviewing files that changed from the base of the PR and between 9acae0c and b821b57.

📒 Files selected for processing (11)
  • src/components/home/QuizBankPreview.tsx
  • src/components/quiz-detail/AttemptStats.tsx
  • src/components/quiz-detail/MetaEditor.tsx
  • src/components/quiz-detail/QuestionEditor.tsx
  • src/hooks/useQuizBank.test.ts
  • src/hooks/useQuizBank.ts
  • src/hooks/useQuizDetail.ts
  • src/utils/attempts.ts
  • src/views/CreateQuiz.tsx
  • src/views/QuizDetailView.tsx
  • supabase/migrations/20260728120000_add_attempt_counts_rpc.sql
📝 Walkthrough

Walkthrough

Adds a dynamic quiz management page with metadata editing, question editing and deletion, attempt statistics, link copying, quiz deletion, CSV question previews, and enriched quiz-bank attempt counts. Quiz cards now link to the dashboard management route.

Changes

Quiz management dashboard

Layer / File(s) Summary
Quiz detail data and route
app/dashboard/quiz/[quizId]/page.tsx, src/hooks/useQuizDetail.ts
Adds the dynamic route and Supabase-backed loading, metadata updates, question mutations, quiz deletion, attempt retrieval, and link copying.
Quiz detail view orchestration
src/views/QuizDetailView.tsx
Adds loading and error states, header actions, Questions/Settings/Stats tabs, editor wiring, and two-step quiz deletion with redirect.
Metadata and question editors
src/components/quiz-detail/MetaEditor.tsx, src/components/quiz-detail/QuestionEditor.tsx
Adds editable quiz metadata, visibility-dependent fields, question content and answer editing, save handling, and delete confirmation.
Attempt statistics rendering
src/components/quiz-detail/AttemptStats.tsx
Adds average score and time summaries plus a recent-attempt list with score formatting and threshold-based colors.
Quiz navigation and CSV preview
src/components/QuizCard.tsx, src/views/CreateQuiz.tsx
Changes quiz-card navigation to “Manage” and adds a parsed-question preview to the CSV creation tab.
Quiz-bank attempt count enrichment
src/utils/attempts.ts, src/hooks/useQuizBank.ts, src/hooks/useQuizzes.ts, src/components/home/QuizBankPreview.tsx, src/hooks/useQuizBank.test.ts, src/test-utils/chain.ts
Adds attempt-count queries, enriches quiz results, renders QuizBankCard, and updates test query-chain setup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor Creator
  participant QuizDetailPage
  participant QuizDetailView
  participant useQuizDetail
  participant Supabase
  participant QuestionEditor
  Creator->>QuizDetailPage: Open quiz management route
  QuizDetailPage->>QuizDetailView: Pass quizId
  QuizDetailView->>useQuizDetail: Fetch quiz detail
  useQuizDetail->>Supabase: Query quiz, questions, and attempts
  Supabase-->>useQuizDetail: Return quiz detail data
  useQuizDetail-->>QuizDetailView: Provide state and handlers
  QuizDetailView->>QuestionEditor: Render editable question
  Creator->>QuestionEditor: Save question
  QuestionEditor->>useQuizDetail: Submit updated question
  useQuizDetail->>Supabase: Update question record
  Supabase-->>useQuizDetail: Confirm update
Loading

Possibly related PRs

  • Truella/prep#3: Also changes CreateQuiz.tsx CSV-tab behavior.
  • Truella/prep#7: Also changes quiz-card actions and quiz-bank UI.
  • Truella/prep#10: Adds attempt data consumed by quiz statistics and attempt-count flows.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a new quiz detail screen with create/read/update/delete management features.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/quiz-detail-crud

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/quiz-detail/AttemptStats.tsx`:
- Around line 77-81: Update AttemptStats and the underlying Pick in
useQuizDetail so each attempt includes its id, then replace the index-based key
in the attempts.map rendering with the attempt’s stable id. Preserve the
existing descending order and display behavior.
- Line 89: Update the date rendering in AttemptStats to avoid calling
toLocaleDateString() during render, matching the hydration-safe approach used by
QuizDetailView. Use a deterministic date format or the existing shared formatter
so server and client produce identical output.
- Around line 23-31: Guard score percentage calculations in AttemptStats,
including the avgScore reduce and the per-row percentage near the row rendering,
so attempts with total_points === 0 do not divide by zero or propagate NaN.
Preserve normal percentage calculations for positive totals and use the
component’s appropriate zero-score fallback consistently in both locations.

In `@src/components/quiz-detail/MetaEditor.tsx`:
- Around line 59-249: Associate every form label in MetaEditor with its control
by assigning matching unique id/htmlFor pairs to the title input, description
textarea, time-limit checkbox and number input, visibility radios, category
select, and difficulty select. Ensure each radio receives a distinct id while
retaining the existing shared name and behavior, and keep the current labels and
handlers unchanged.
- Line 19: Update the MetaEditor onSave prop to use the same specific
Partial<Pick<...>> metadata-update type accepted by
useQuizDetail.updateQuizMeta, covering only title, description, time_limit,
visibility, category, and difficulty; avoid the broad Record<string, unknown>
type.
- Around line 114-131: Update the time-limit input in MetaEditor so onChange
only updates the displayed value, and defer onSave({ time_limit: ... }) until
onBlur, matching the title/description persistence behavior. Preserve the
existing 1–180 validation and null-rendering behavior.

In `@src/components/quiz-detail/QuestionEditor.tsx`:
- Around line 89-215: Update the buttons in QuestionEditor, including the
Edit/Delete controls, correct-answer toggle within OPTION_KEYS.map, Save
question, and Cancel buttons, to explicitly declare their intended HTML button
type; use type="button" for controls that should not submit a form and preserve
the existing save behavior appropriately.
- Around line 178-190: Clamp the parsed points value in the QuestionEditor
input’s onChange handler to the inclusive range 1–100 before updating
draft.points. Preserve the existing fallback for empty or invalid input while
ensuring negative values and values above 100 cannot be saved.

In `@src/hooks/useQuizDetail.ts`:
- Around line 87-109: Update the updateQuizMeta parameter type to use
NonNullable<QuizDetailState["quiz"]> instead of QuizDetailState["quiz"] & object
when defining the selected fields, preserving the existing update behavior and
field restrictions.
- Around line 172-177: Update the copyLink function to await
navigator.clipboard.writeText and show the success toast only after it resolves;
catch rejected writes and handle them with an appropriate failure path instead
of reporting success.
- Around line 43-84: Update fetchQuizDetail to guard against stale asynchronous
responses when quizId changes or the effect/refetch runs again. Track the latest
request or use an abort/ignore mechanism, and check it before each state update
so only the current request can modify state; preserve the existing
quiz-not-found and successful data behavior.
- Around line 61-79: Handle the errors returned by the questions and
quiz_attempts queries in the quiz detail loading flow before updating state,
alongside the existing quizError handling. Surface any query failure through the
existing error state and avoid rendering empty questions or attempts when their
fetch fails; retain the successful mapping and state update behavior otherwise.

In `@src/views/CreateQuiz.tsx`:
- Around line 143-168: Update the option rendering inside the options.map
callback to visibly display a “Correct” label whenever j === q.correctIndex,
alongside the existing option content. Keep the current color styling and ensure
incorrect options do not show the label.

In `@src/views/QuizDetailView.tsx`:
- Around line 107-227: Set type="button" on the Copy link button, every tab
button rendered by TABS.map, and the danger-zone delete and cancel buttons in
QuizDetailView. Keep their existing click handlers and behavior unchanged.
- Around line 98-104: Update the date rendering in QuizDetailView to use
deterministic, locale- and timezone-independent formatting instead of new
Date(quiz.created_at).toLocaleDateString(). Preserve the existing created-date
display while ensuring SSR and client hydration produce identical text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 50ff64cf-324a-4be4-999a-394a1a568b2a

📥 Commits

Reviewing files that changed from the base of the PR and between 3439398 and b1a9cf0.

📒 Files selected for processing (8)
  • app/dashboard/quiz/[quizId]/page.tsx
  • src/components/QuizCard.tsx
  • src/components/quiz-detail/AttemptStats.tsx
  • src/components/quiz-detail/MetaEditor.tsx
  • src/components/quiz-detail/QuestionEditor.tsx
  • src/hooks/useQuizDetail.ts
  • src/views/CreateQuiz.tsx
  • src/views/QuizDetailView.tsx

Comment thread src/components/quiz-detail/AttemptStats.tsx
Comment thread src/components/quiz-detail/AttemptStats.tsx Outdated
Comment thread src/components/quiz-detail/AttemptStats.tsx Outdated
Comment thread src/components/quiz-detail/MetaEditor.tsx Outdated
Comment thread src/components/quiz-detail/MetaEditor.tsx
Comment thread src/hooks/useQuizDetail.ts
Comment thread src/hooks/useQuizDetail.ts
Comment thread src/views/CreateQuiz.tsx
Comment thread src/views/QuizDetailView.tsx
Comment thread src/views/QuizDetailView.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
src/hooks/useQuizDetail.ts (1)

52-57: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distinguish missing quizzes from load failures
quizError is treated the same as an absent row here, so network, RLS, and server errors are surfaced as "Quiz not found". Handle the not-found case separately and show a load-failure message for other errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useQuizDetail.ts` around lines 52 - 57, Update the error handling
in useQuizDetail so quizError and missing quizData are handled separately: keep
“Quiz not found” only when the query succeeds without a row, and use a
load-failure message when quizError is present. Preserve loading=false and the
existing state update structure for both outcomes.
src/hooks/useQuizBank.test.ts (1)

129-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mock the quiz_attempts query separately and assert enrichment.

This fixture is also returned for getAttemptCounts, but its rows have id, not quiz_id; the reducer therefore produces no usable counts. Mock from("quizzes") and from("quiz_attempts") independently, then assert that each returned quiz receives the expected times_taken value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useQuizBank.test.ts` around lines 129 - 140, Update the test around
useQuizBank so supabase.from returns separate chains for “quizzes” and
“quiz_attempts”, using attempt rows with quiz_id values that match the fixture
quizzes. Assert the client-side search result still contains only “Math Quiz”
and verify its enriched times_taken value, ensuring the attempt-count reducer
receives valid data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/useQuizBank.ts`:
- Around line 68-74: In the quiz-loading flow around getAttemptCounts and
setAllQuizzes, re-check currentGen after the awaited count request and before
mutating state; return or skip the stale result when the generation changed.
Only clear the loading state after the current request commits its enriched
quizzes, preserving the existing enrichment behavior.

In `@src/utils/attempts.ts`:
- Around line 8-13: Update the quiz-attempts query flow around the Supabase call
in the attempts utility to inspect and propagate its error instead of treating
missing data as an empty result. Return a distinct failure outcome, then update
both callers to preserve their existing attempt counts or display an error state
when the query fails, while retaining zero counts for successful queries with no
matching attempts.
- Around line 8-18: Update the attempt-counting query in the visible Supabase
flow to aggregate counts by quiz_id in SQL, using an appropriate view or RPC
that performs COUNT(*) and GROUP BY quiz_id. Refactor the result handling in the
surrounding function to map the aggregated rows into the existing Record<string,
number> shape, without fetching individual quiz_attempts rows or relying on
client-side reduce counting.

---

Outside diff comments:
In `@src/hooks/useQuizBank.test.ts`:
- Around line 129-140: Update the test around useQuizBank so supabase.from
returns separate chains for “quizzes” and “quiz_attempts”, using attempt rows
with quiz_id values that match the fixture quizzes. Assert the client-side
search result still contains only “Math Quiz” and verify its enriched
times_taken value, ensuring the attempt-count reducer receives valid data.

In `@src/hooks/useQuizDetail.ts`:
- Around line 52-57: Update the error handling in useQuizDetail so quizError and
missing quizData are handled separately: keep “Quiz not found” only when the
query succeeds without a row, and use a load-failure message when quizError is
present. Preserve loading=false and the existing state update structure for both
outcomes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 60a91c3e-d717-4a35-b7b3-515298c399d1

📥 Commits

Reviewing files that changed from the base of the PR and between b1a9cf0 and 74b378c.

📒 Files selected for processing (7)
  • app/dashboard/quiz/[quizId]/page.tsx
  • src/components/home/QuizBankPreview.tsx
  • src/hooks/useQuizBank.test.ts
  • src/hooks/useQuizBank.ts
  • src/hooks/useQuizDetail.ts
  • src/test-utils/chain.ts
  • src/utils/attempts.ts

Comment thread src/hooks/useQuizBank.ts
Comment thread src/utils/attempts.ts Outdated
Comment thread src/utils/attempts.ts Outdated
…nd enhance useQuizzes to fetch attempt counts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/useQuizzes.ts`:
- Around line 53-55: Update getAttemptCounts, used by useQuizzes, to aggregate
attempt totals in the database or through an RPC grouped by quiz ID, returning
one count per requested ID. Preserve the existing counts mapping contract so
callers receive accurate times_taken values without fetching individual attempt
rows or reducing them in the browser.
- Around line 55-57: Update getAttemptCounts to propagate Supabase query errors
instead of returning an empty object. Preserve the existing successful counts
mapping so useQuizzes can continue assigning counts via counts[q.id] ?? 0 only
when the query succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: af1e74f5-624a-44ab-be71-da3708e5a25b

📥 Commits

Reviewing files that changed from the base of the PR and between 74b378c and 9acae0c.

📒 Files selected for processing (2)
  • src/components/QuizCard.tsx
  • src/hooks/useQuizzes.ts

Comment thread src/hooks/useQuizzes.ts
Comment thread src/hooks/useQuizzes.ts
Truella and others added 4 commits July 28, 2026 12:16
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Mistura <101476258+Truella@users.noreply.github.com>
@Truella
Truella merged commit 308cb1d into master Jul 28, 2026
5 checks passed
@Truella
Truella deleted the feat/quiz-detail-crud branch July 28, 2026 11:41
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