Skip to content

My votes availability#1942

Merged
simo6529 merged 4 commits intomainfrom
my-votes-availability
Feb 17, 2026
Merged

My votes availability#1942
simo6529 merged 4 commits intomainfrom
my-votes-availability

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented Feb 17, 2026

Summary by CodeRabbit

  • New Features

    • Optimistic voting: vote values update immediately and UI shows Available and Max vote info.
    • New layout: max value shown above input with submit control and clearer Available/Max display.
  • Bug Fixes

    • Improved draft handling, clamping, and validation for vote edits.
    • Ensures leaderboard state refresh after successful votes and handles missing server context.
  • Tests

    • Expanded coverage for voting flows, optimistic updates, UI states, and edge cases.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Adds optimistic voting and draft state to MyStreamWaveMyVoteInput with live vote/max calculations and query-cache invalidation; introduces availableVotes propagation to MyStreamWaveMyVotesReset and expands tests to cover optimistic flows, UI text, draft handling, and react-query mocks.

Changes

Cohort / File(s) Summary
Optimistic Voting Logic
components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx
Implements OptimisticVoteState and VoteDraftState, computes liveCurrentVoteValue/liveMaxRating, clamps inputs to liveMaxRating, supports draft editing, submits votes with useQueryClient and invalidates DROPS_LEADERBOARD, and updates UI layout to show Available/Max and a submit area.
Available Votes Propagation
components/brain/my-stream/votes/MyStreamWaveMyVotes.tsx, components/brain/my-stream/votes/MyStreamWaveMyVotesReset.tsx
Computes sharedAvailableVotes from drops, passes as availableVotes prop to MyStreamWaveMyVotesReset; MyStreamWaveMyVotesReset adds optional `availableVotes?: number
Tests — Input Component
__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx
Extensive test updates: add react-query mocks (useMutation/useQueryClient), provide ReactQueryWrapperContext, standardize fixtures, and add multiple cases covering optimistic updates, clamping, fallback when server lacks context, draft reset on live changes, and cache invalidation assertions.
Tests — Votes List & Reset
__tests__/components/brain/my-stream/votes/MyStreamWaveMyVotes.test.tsx, __tests__/components/brain/my-stream/votes/MyStreamWaveMyVotesReset.test.tsx
Adjusts mocks for child components, captures resetProps to assert availableVotes propagated, adds tests for available votes rendering/visibility, and updates fixtures to include context_profile_context where needed.
Formatting & Minor Mocks
__tests__/... (multiple test files)
Consistent use of double-quoted import paths and minor formatting/expectation tweaks across tests and mocks.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Component as MyStreamWaveMyVoteInput
    participant QueryClient as QueryClient
    participant Server as Server

    User->>Component: Enter value & submit
    activate Component
    Component->>Component: Clamp to liveMaxRating\nUpdate optimistic state & draft
    Component->>User: Show optimistic UI (Available / Max updated)
    Component->>Server: mutateAsync POST vote
    deactivate Component

    activate Server
    Server->>Server: Process vote, build response (may include context_profile_context)
    Server-->>Component: Return response
    deactivate Server

    activate Component
    Component->>QueryClient: invalidateQueries(DROPS_LEADERBOARD)
    Component->>Component: Apply server context to optimistic state\nClear or adjust draft
    Component->>User: Reflect final server-backed UI
    deactivate Component
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • prxt6529

Poem

🐰
Hops and drafts and hopeful votes,
I nibble numbers, stitch the quotes.
Optimistic whiskers tap the UI,
Cache refreshed — a little sigh.
Tiny paws applaud each submit.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "My votes availability" directly relates to the main changes in the PR, which implement optimistic voting flow with available votes tracking and display across multiple voting components.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch my-votes-availability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx (1)

152-168: ⚠️ Potential issue | 🟡 Minor

No early return when clamped value equals current vote — submits a no-op API call.

When clampedValue === liveCurrentVoteValue (line 164), the draft is cleared but execution continues into setIsProcessing(true)requestAuthmutateAsync. The button is disabled via !isEditing, but handleSubmit is also reachable via the Enter key handler (line 202), bypassing that guard. This results in an unnecessary API round-trip submitting the same vote value.

Proposed fix
     if (clampedValue === liveCurrentVoteValue) {
       setVoteDraftState(null);
+      return;
-    } else {
-      setVoteDraftValue(String(clampedValue));
     }
+    setVoteDraftValue(String(clampedValue));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx` around lines
152 - 168, The submit handler handleSubmit currently clears the draft when
clampedValue === liveCurrentVoteValue but continues execution and triggers
setIsProcessing/requestAuth/mutateAsync, causing a no-op API call; change
handleSubmit so that after setting setVoteDraftState(null) it returns
immediately (early exit) to prevent further processing and the mutateAsync call
when liveCurrentVoteValue equals the clamped parsedVoteValue; keep existing
guards for isProcessing/isResetting and ensure functions referenced
(setVoteDraftState, setVoteDraftValue, setIsProcessing, requestAuth,
mutateAsync, liveCurrentVoteValue, parsedVoteValue) remain unchanged otherwise.
🧹 Nitpick comments (2)
__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx (1)

8-11: Full module mock replaces all react-query exports.

This mock replaces the entire @tanstack/react-query module, so any export besides useMutation and useQueryClient used transitively would be undefined. Currently the component only imports those two, so this is fine, but it's fragile if additional hooks are added later. Consider using jest.requireActual to preserve the rest:

Optional improvement
 jest.mock("@tanstack/react-query", () => ({
+  ...jest.requireActual("@tanstack/react-query"),
   useMutation: jest.fn(),
   useQueryClient: jest.fn(),
 }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx`
around lines 8 - 11, The jest.mock call currently replaces the entire
`@tanstack/react-query` module which can unintentionally stub other exports;
update the mock in MyStreamWaveMyVoteInput.test.tsx to call
jest.requireActual('@tanstack/react-query') and only override useMutation and
useQueryClient so other exports remain intact. Locate the existing
jest.mock(...) and change it to merge the real module with mocked
implementations for useMutation and useQueryClient (keeping their jest.fn()
behavior) so future added hooks still work.
components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx (1)

113-150: Consider potential double error toast when mutateAsync rejects.

The onError callback (pre-existing, line 143) calls setToast and re-throws. Since handleSubmit uses mutateAsync, the rejection also propagates to the catch block at line 186, which calls setToast again. This results in two error toasts for a single failure. The throw error in onError could also cause an unhandled rejection depending on the TanStack Query version.

If you're touching this mutation anyway, consider either removing the onError handler (letting handleSubmit's catch be the sole handler) or removing the duplicate toast from handleSubmit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx` around lines
113 - 150, The mutation's onError currently both sets a toast and re-throws,
causing duplicate toasts and possible unhandled rejections; remove the onError
handler from the rateChangeMutation (the useMutation call) so errors propagate
to handleSubmit's mutateAsync catch block which is the single place to set the
error toast; locate rateChangeMutation (useMutation) and delete the entire
onError: (error) => { ... } block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx`:
- Around line 47-68: The computed availableVotes can exceed liveMaxRating when
liveCurrentVoteValue is negative; modify the calculation of availableVotes
(current variable in MyStreamWaveMyVoteInput.tsx) to clamp the result between 0
and liveMaxRating (e.g., use Math.min(liveMaxRating, Math.max(0, liveMaxRating -
liveCurrentVoteValue))) so "available" never exceeds the configured max; update
any dependent UI/labels if they rely on the previous unbounded value.
- Around line 238-256: The spinner SVG uses absolute positioning (tw-absolute)
but its parent <button> in MyStreamWaveMyVoteInput.tsx lacks a positioned
ancestor, so add the positioning class (e.g., "tw-relative") to the button
element that wraps the spinner (the button rendered alongside
isProcessing/isResetting) so the svg is positioned correctly; update the
button's className to include "tw-relative" (preserve existing classes) so the
spinner stays inside the button bounds.

---

Outside diff comments:
In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx`:
- Around line 152-168: The submit handler handleSubmit currently clears the
draft when clampedValue === liveCurrentVoteValue but continues execution and
triggers setIsProcessing/requestAuth/mutateAsync, causing a no-op API call;
change handleSubmit so that after setting setVoteDraftState(null) it returns
immediately (early exit) to prevent further processing and the mutateAsync call
when liveCurrentVoteValue equals the clamped parsedVoteValue; keep existing
guards for isProcessing/isResetting and ensure functions referenced
(setVoteDraftState, setVoteDraftValue, setIsProcessing, requestAuth,
mutateAsync, liveCurrentVoteValue, parsedVoteValue) remain unchanged otherwise.

---

Nitpick comments:
In `@__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx`:
- Around line 8-11: The jest.mock call currently replaces the entire
`@tanstack/react-query` module which can unintentionally stub other exports;
update the mock in MyStreamWaveMyVoteInput.test.tsx to call
jest.requireActual('@tanstack/react-query') and only override useMutation and
useQueryClient so other exports remain intact. Locate the existing
jest.mock(...) and change it to merge the real module with mocked
implementations for useMutation and useQueryClient (keeping their jest.fn()
behavior) so future added hooks still work.

In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx`:
- Around line 113-150: The mutation's onError currently both sets a toast and
re-throws, causing duplicate toasts and possible unhandled rejections; remove
the onError handler from the rateChangeMutation (the useMutation call) so errors
propagate to handleSubmit's mutateAsync catch block which is the single place to
set the error toast; locate rateChangeMutation (useMutation) and delete the
entire onError: (error) => { ... } block.

Comment thread components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx Outdated
Comment thread components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (1)
components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx (1)

151-167: ⚠️ Potential issue | 🟡 Minor

handleSubmit proceeds to API call even when the value hasn't changed.

When clampedValue === liveCurrentVoteValue (lines 163-164), the draft is cleared but execution falls through to setIsProcessing(true)requestAuthmutateAsync. This fires a redundant API call and shows a "Vote updated" toast for a no-op. The button's disabled state guards against click, but the Enter-key path (handleKeyDown) doesn't check isEditing.

Proposed fix — early-return when nothing changed
     const clampedValue = Math.min(
       Math.max(parsedVoteValue, minRating),
       liveMaxRating
     );
     if (clampedValue === liveCurrentVoteValue) {
       setVoteDraftState(null);
+      return;
     } else {
       setVoteDraftValue(String(clampedValue));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx` around lines
151 - 167, The handleSubmit function currently clears the draft when
clampedValue === liveCurrentVoteValue but continues and triggers the API flow
(setIsProcessing, requestAuth, mutateAsync); change handleSubmit (in
MyStreamWaveMyVoteInput) to return early after setting setVoteDraftState(null)
when clampedValue equals liveCurrentVoteValue so it does not call
setIsProcessing/requestAuth/mutateAsync for a no-op vote update; ensure the
early-return covers both click and Enter-key paths (handleSubmit called from
handleKeyDown) so redundant API calls and "Vote updated" toasts are prevented.
🧹 Nitpick comments (2)
__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx (1)

79-88: Consider adding a test for Enter-key submission.

The component supports submitting via Enter (handleKeyDown), which bypasses the button's disabled guard on isEditing. A test verifying that pressing Enter with an unchanged value doesn't fire a mutation would strengthen coverage — especially given the missing early-return noted in the production code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx`
around lines 79 - 88, Add a test that exercises the component's Enter-key
submission path (handleKeyDown) in MyStreamWaveMyVoteInput: render the component
(same setup as the existing test), focus the textbox and
fireEvent.keyDown(input, { key: "Enter", code: "Enter" }) when the input value
is unchanged and isEditing is true, then assert that auth.requestAuth and
mutateAsync were NOT called; also add a complementary assertion that when the
value is changed (e.g., fireEvent.change to a clamped value) pressing Enter does
call auth.requestAuth and mutateAsync with the expected { rate } to verify
correct behavior.
components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx (1)

140-148: Double toast on mutation error.

onError (line 142-146) calls setToast and then throw error re-throws into the catch block (line 185-193), which calls setToast again with a potentially different message. The second call overwrites the first, so the user sees only the catch block's message. This is pre-existing behavior, but now that the component is being reworked, consider removing the throw error from onError or removing the duplicate setToast in the catch block.

Option: let onError handle the toast, remove re-throw
     onError: (error) => {
       setToast({
         message: error as unknown as string,
         type: "error",
       });
-      throw error;
     },

Then in handleSubmit, the catch would only handle truly unexpected errors (e.g., network failures before the mutation fires).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx` around lines
140 - 148, The mutation's onError callback (onError in the mutation options) is
calling setToast then re-throwing the error, which causes the catch in
handleSubmit to show a second toast; remove the throw error from onError so
onError fully handles user-visible errors and the catch block in handleSubmit
only handles unexpected failures (or alternatively remove the duplicate setToast
in handleSubmit's catch if you prefer to surface errors there), ensuring
references to onError, setToast, and handleSubmit are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx`:
- Around line 151-167: The handleSubmit function currently clears the draft when
clampedValue === liveCurrentVoteValue but continues and triggers the API flow
(setIsProcessing, requestAuth, mutateAsync); change handleSubmit (in
MyStreamWaveMyVoteInput) to return early after setting setVoteDraftState(null)
when clampedValue equals liveCurrentVoteValue so it does not call
setIsProcessing/requestAuth/mutateAsync for a no-op vote update; ensure the
early-return covers both click and Enter-key paths (handleSubmit called from
handleKeyDown) so redundant API calls and "Vote updated" toasts are prevented.

---

Nitpick comments:
In `@__tests__/components/brain/my-stream/votes/MyStreamWaveMyVoteInput.test.tsx`:
- Around line 79-88: Add a test that exercises the component's Enter-key
submission path (handleKeyDown) in MyStreamWaveMyVoteInput: render the component
(same setup as the existing test), focus the textbox and
fireEvent.keyDown(input, { key: "Enter", code: "Enter" }) when the input value
is unchanged and isEditing is true, then assert that auth.requestAuth and
mutateAsync were NOT called; also add a complementary assertion that when the
value is changed (e.g., fireEvent.change to a clamped value) pressing Enter does
call auth.requestAuth and mutateAsync with the expected { rate } to verify
correct behavior.

In `@components/brain/my-stream/votes/MyStreamWaveMyVoteInput.tsx`:
- Around line 140-148: The mutation's onError callback (onError in the mutation
options) is calling setToast then re-throwing the error, which causes the catch
in handleSubmit to show a second toast; remove the throw error from onError so
onError fully handles user-visible errors and the catch block in handleSubmit
only handles unexpected failures (or alternatively remove the duplicate setToast
in handleSubmit's catch if you prefer to surface errors there), ensuring
references to onError, setToast, and handleSubmit are updated accordingly.

@simo6529 simo6529 merged commit 243d458 into main Feb 17, 2026
7 checks passed
@simo6529 simo6529 deleted the my-votes-availability branch February 17, 2026 20:21
@coderabbitai coderabbitai Bot mentioned this pull request Apr 13, 2026
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