Skip to content

grant rep modal dropdown#1994

Merged
ragnep merged 11 commits intomainfrom
grant-rep-modal-ux
Feb 27, 2026
Merged

grant rep modal dropdown#1994
ragnep merged 11 commits intomainfrom
grant-rep-modal-ux

Conversation

@ragnep
Copy link
Copy Markdown
Contributor

@ragnep ragnep commented Feb 27, 2026

Summary by CodeRabbit

  • Bug Fixes

    • More reliable avatar display with graceful fallback when profile images fail to load.
  • Style

    • Updated Grant Rep button visuals (border, background, hover, icon tone).
    • Tweaked spacing and layout across dialogs and the rep/search form.
  • Refactor

    • Simplified rep-search state handling and dropdown rendering for clearer behavior.
  • New Features

    • Added an optional Cancel action to the rep/grant flow.
  • Tests

    • Updated test imports to reflect internal refactoring.

Signed-off-by: ragnep <ragneinfo@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors avatar rendering into an internal AvatarContent with image-error fallback; tweaks Grant Rep button styling; simplifies GrantRepDialog layout and routes Cancel via child onCancel; moves RepSearchState into a new enum module; converts rep-search state to derived/memoized values, simplifies dropdown rendering, and threads optional onCancel through rep components.

Changes

Cohort / File(s) Summary
Avatar Refactor
components/common/OverlappingAvatars.tsx
Extracted AvatarContent component with internal imgError state; replaced inline avatar JSX with AvatarContent usage and added useState import.
Rep Header Styling
components/user/rep/header/UserPageRepHeader.tsx
Adjusted Grant Rep button styles and SVG color (border opacity, subtle white background tint, hover intensity, icon color).
Grant Rep Dialog
components/user/rep/new-rep/GrantRepDialog.tsx
Removed outer padded wrapper and explicit Cancel button; now renders UserPageRepNewRep inside UserPageRateWrapper and forwards close via onCancel/onSuccess.
Rep New Rep API / Props
components/user/rep/new-rep/UserPageRepNewRep.tsx
Added optional onCancel?: () => void prop and forwarded it to UserPageRepNewRepSearch.
Rep Search Logic & UI
components/user/rep/new-rep/UserPageRepNewRepSearch.tsx, components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.tsx, components/user/rep/new-rep/rep-search-types.ts
Added RepSearchState enum file; replaced local enum with import; converted several stateful effects to derived booleans/useMemo; simplified dropdown to early-return cases (min/max errors, loading, empty) and render category buttons; added CircleLoader; added optional onCancel UI/logic; normalized error handling and endpoint guards.
Tests
__tests__/components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.test.tsx
Updated test import for RepSearchState to new rep-search-types module.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • prxt6529
  • simo6529

Poem

🐰 I mended little faces, fixed a faded eye,
Routed cancels through softly so buttons don’t cry,
Dropdowns hush and wait while loaders hum low,
Memoized my carrots so the flow can just go,
Hop, hop — a tiny tweak, off to nibble a rye! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'grant rep modal dropdown' is directly related to the main changes in this PR, which refactor the grant rep modal UI and update the dropdown rendering logic with improved state handling.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch grant-rep-modal-ux

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.

🧹 Nitpick comments (1)
components/common/OverlappingAvatars.tsx (1)

37-70: avatarRing prop is ignored in the fallback case.

The avatarRing prop is accepted but only applied to the successful image render (line 67). The fallback <div> (lines 52-56) uses hardcoded ring styling instead of the passed prop. While the current hardcoded values happen to match the usage at line 83-84, this creates a maintenance risk if the ring style needs to change.

♻️ Suggested fix to apply avatarRing consistently
   if (!pfpUrl || imgError) {
     return (
-      <div className="tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-rounded-full tw-bg-iron-700 tw-ring-[1.5px] tw-ring-black">
+      <div className={`tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center ${avatarRing}`}>
         <span className="tw-text-[0.625rem] tw-font-semibold tw-text-iron-300">
           {fallback ?? "?"}
         </span>
       </div>
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/common/OverlappingAvatars.tsx` around lines 37 - 70, The
AvatarContent component ignores the avatarRing prop for the fallback UI: when
pfpUrl is missing or imgError is true the fallback <div> uses hardcoded ring
classes instead of using avatarRing. Fix AvatarContent by applying the
avatarRing variable to the fallback div’s className (e.g., merge avatarRing with
the existing layout classes) and remove or replace the hardcoded ring classes
(tw-ring-[1.5px] tw-ring-black) so the same ring styling is used whether
rendering the <img> or the fallback; keep other classes (tw-flex,
tw-rounded-full, etc.) and ensure ariaLabel/fallback behavior stays unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/common/OverlappingAvatars.tsx`:
- Around line 37-70: The AvatarContent component ignores the avatarRing prop for
the fallback UI: when pfpUrl is missing or imgError is true the fallback <div>
uses hardcoded ring classes instead of using avatarRing. Fix AvatarContent by
applying the avatarRing variable to the fallback div’s className (e.g., merge
avatarRing with the existing layout classes) and remove or replace the hardcoded
ring classes (tw-ring-[1.5px] tw-ring-black) so the same ring styling is used
whether rendering the <img> or the fallback; keep other classes (tw-flex,
tw-rounded-full, etc.) and ensure ariaLabel/fallback behavior stays unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d69c8a1 and 4cf61b1.

📒 Files selected for processing (5)
  • components/common/OverlappingAvatars.tsx
  • components/user/rep/header/UserPageRepHeader.tsx
  • components/user/rep/new-rep/GrantRepDialog.tsx
  • components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
  • components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.tsx

Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
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.

🧹 Nitpick comments (1)
components/common/OverlappingAvatars.tsx (1)

61-71: Duplicate CSS classes in className.

Line 69 includes tw-object-cover tw-rounded-full explicitly, but avatarRing (defined at line 85-86) already contains both of these classes. This results in redundant class names in the output.

♻️ Suggested fix
     <Image
       src={getScaledImageUri(pfpUrl, ImageScale.W_AUTO_H_50)}
       alt={ariaLabel ?? "Profile"}
       fill
       sizes="28px"
       unoptimized
       onError={() => setImgError(true)}
-      className={`tw-object-cover tw-rounded-full ${avatarRing}`}
+      className={avatarRing}
     />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/common/OverlappingAvatars.tsx` around lines 61 - 71, The Image
component in OverlappingAvatars.tsx is producing duplicate CSS classes because
you append "tw-object-cover tw-rounded-full" while avatarRing already contains
those; remove the redundancy by using only avatarRing in the Image's className
(or alternatively remove those classes from avatarRing and keep them inline) so
that className for the Image is not duplicating styles; update the Image JSX
(and/or the avatarRing definition) accordingly and ensure setImgError/onError
handling remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/common/OverlappingAvatars.tsx`:
- Around line 61-71: The Image component in OverlappingAvatars.tsx is producing
duplicate CSS classes because you append "tw-object-cover tw-rounded-full" while
avatarRing already contains those; remove the redundancy by using only
avatarRing in the Image's className (or alternatively remove those classes from
avatarRing and keep them inline) so that className for the Image is not
duplicating styles; update the Image JSX (and/or the avatarRing definition)
accordingly and ensure setImgError/onError handling remains unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf61b1 and f3d8fb5.

📒 Files selected for processing (1)
  • components/common/OverlappingAvatars.tsx

Signed-off-by: ragnep <ragneinfo@gmail.com>
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

🧹 Nitpick comments (1)
components/user/rep/new-rep/UserPageRepNewRepSearch.tsx (1)

101-110: Consider using a primitive value to key the prefill instead of object identity.

The render-phase prefill pattern here is safe with useRepAllocation because repState is a stable state value—it only changes when the useEffect dependencies change. However, relying on object identity comparison is fragile. Since you need to detect when rater_contribution changes, key off that primitive instead:

const [prevContribution, setPrevContribution] = useState(repState?.rater_contribution);
if (repState?.rater_contribution !== prevContribution) {
  setPrevContribution(repState.rater_contribution);
  setAmountStr(`${repState.rater_contribution}`);
}

This is more resilient and makes the intent clearer.

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

In `@components/user/rep/new-rep/UserPageRepNewRepSearch.tsx` around lines 101 -
110, Replace the object-identity prefill check using prevRepState with a
primitive-based check keyed on repState.rater_contribution: track a
prevContribution via useState and compare repState?.rater_contribution to
prevContribution; when they differ call
setPrevContribution(repState.rater_contribution) and
setAmountStr(`${repState.rater_contribution}`). Update references to
prevRepState/setPrevRepState to use prevContribution/setPrevContribution and
remove the old object comparison logic that used repState identity.
🤖 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/user/rep/new-rep/UserPageRepNewRepSearch.tsx`:
- Around line 200-204: The submit handler onSearchSubmit is using debouncedValue
which can ignore Enter presses inside the debounce window; change it to use the
live input state (the uncontrolled/controlled input variable used to track the
current text, e.g., value or searchValue) instead of debouncedValue, keep
event.preventDefault(), and call onRepSelect with the current live input
(optionally trimmed) so Enter always triggers a submit regardless of debounce.
- Around line 161-162: The endpoint construction in UserPageRepNewRepSearch
currently uses `profiles/${profile.query ?? ""}/rep/rating`, which can produce
an invalid path when profile.query is missing; update the submit flow (where
endpoint is built) to validate that `profile.query` is present before posting
and fail fast (return/throw or disable the submit) if it's falsy, e.g., guard in
the submit handler or form validation inside the component so you never call the
API with an empty profile segment; ensure any error path logs or surfaces a
clear message and reference the same identifiers (`profile.query` and the
endpoint assembly) when implementing the guard.

---

Nitpick comments:
In `@components/user/rep/new-rep/UserPageRepNewRepSearch.tsx`:
- Around line 101-110: Replace the object-identity prefill check using
prevRepState with a primitive-based check keyed on repState.rater_contribution:
track a prevContribution via useState and compare repState?.rater_contribution
to prevContribution; when they differ call
setPrevContribution(repState.rater_contribution) and
setAmountStr(`${repState.rater_contribution}`). Update references to
prevRepState/setPrevRepState to use prevContribution/setPrevContribution and
remove the old object comparison logic that used repState identity.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3d8fb5 and 03e2405.

📒 Files selected for processing (6)
  • __tests__/components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.test.tsx
  • components/user/rep/new-rep/GrantRepDialog.tsx
  • components/user/rep/new-rep/UserPageRepNewRep.tsx
  • components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
  • components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.tsx
  • components/user/rep/new-rep/rep-search-types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/user/rep/new-rep/GrantRepDialog.tsx
  • components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.tsx

Comment thread components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
Comment thread components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
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

🤖 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/user/rep/new-rep/UserPageRepNewRepSearch.tsx`:
- Around line 130-133: The onRepSelect handler (and the submit flow around the
same input usage) currently validates and submits the raw rep string; trim the
input first to avoid accepting whitespace-only or padded values—i.e., call
.trim() on the incoming rep value before comparing against SEARCH_LENGTH.MIN/MAX
in onRepSelect and use the trimmed value when passing to the submit function
(the same change should be applied where the input is read/submitted around
lines 201–205), ensuring all validations and downstream calls reference the
trimmedRep variable instead of the original raw input.
- Around line 101-110: Move the prefill logic out of render into a useEffect:
stop comparing and calling setPrevRepState/setAmountStr during render; instead
create a useEffect that depends on selectedCategory (and repState) and inside it
check whether the category has changed (track a lastPrefilledCategory ref/state)
before calling setAmountStr and updating prevRepState; reference the existing
symbols repState, setPrevRepState, setAmountStr, and the
useRepAllocation/selectedCategory to locate the logic and ensure the effect only
runs when selectedCategory changes to avoid overwriting user edits or causing
render-time state updates.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03e2405 and d3b581f.

📒 Files selected for processing (1)
  • components/user/rep/new-rep/UserPageRepNewRepSearch.tsx

Comment thread components/user/rep/new-rep/UserPageRepNewRepSearch.tsx Outdated
Comment thread components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
Signed-off-by: ragnep <ragneinfo@gmail.com>
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.

♻️ Duplicate comments (2)
components/user/rep/new-rep/UserPageRepNewRepSearch.tsx (2)

101-110: ⚠️ Potential issue | 🟠 Major

Move render-time prefill state updates into an effect.

Line 105 updates state during render and compares object identity (repState !== prevRepState), which can cause rerender churn and unexpected amountStr resets when repState reference changes. Prefer useEffect keyed by category transition (or a stable repState key) to avoid render-phase updates.

Suggested refactor
-import { useContext, useMemo, useRef, useState } from "react";
+import { useContext, useEffect, useMemo, useRef, useState } from "react";
@@
-  // Pre-fill amountStr when repState changes (e.g. after category selection).
-  // Uses the React "adjust state during render" pattern so the user can still
-  // freely edit the value after pre-fill.
-  const [prevRepState, setPrevRepState] = useState(repState);
-  if (repState !== prevRepState) {
-    setPrevRepState(repState);
-    if (repState) {
-      setAmountStr(`${repState.rater_contribution}`);
-    }
-  }
+  const lastPrefilledCategoryRef = useRef<string | null>(null);
+  useEffect(() => {
+    if (!selectedCategory || !repState) return;
+    if (lastPrefilledCategoryRef.current === selectedCategory) return;
+    setAmountStr(`${repState.rater_contribution}`);
+    lastPrefilledCategoryRef.current = selectedCategory;
+  }, [selectedCategory, repState]);
#!/bin/bash
set -euo pipefail

# Verify whether `repState` from useRepAllocation is reference-stable or recreated frequently.
HOOK_FILE="$(fd 'useRepAllocation.(ts|tsx)$' | head -n1)"
SEARCH_FILE="$(fd 'UserPageRepNewRepSearch.tsx$' | head -n1)"

echo "=== useRepAllocation source ==="
cat -n "$HOOK_FILE" | sed -n '1,260p'

echo "=== repState construction/memo hints ==="
rg -n -C3 'repState|useMemo|return \{' "$HOOK_FILE"

echo "=== render-time state update sites in UserPageRepNewRepSearch ==="
rg -n -C3 'prevRepState|setPrevRepState|setAmountStr' "$SEARCH_FILE"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/user/rep/new-rep/UserPageRepNewRepSearch.tsx` around lines 101 -
110, The render-time state update should be moved into a useEffect to avoid
changing state during render: replace the inline comparison of repState !==
prevRepState with a useEffect that depends on a stable key (e.g. repState?.id or
repState?.rater_contribution) and inside the effect call setPrevRepState(...)
and setAmountStr(`${repState.rater_contribution}`) only when that key indicates
a real category transition; update references to prevRepState, setPrevRepState,
repState, and setAmountStr accordingly and ensure the effect’s dependency array
uses the stable key (or repState?.rater_contribution) rather than the object
identity.

130-143: ⚠️ Potential issue | 🟡 Minor

Normalize category input before validation and submission.

Line 131 and Line 203 use raw input; whitespace-padded values can pass length checks and be sent downstream. Trim once and use the normalized value consistently in validation, availability check, and state updates.

Suggested fix
   const onRepSelect = async (rep: string) => {
-    if (rep.length < SEARCH_LENGTH.MIN || rep.length > SEARCH_LENGTH.MAX)
+    const normalizedRep = rep.trim();
+    if (
+      normalizedRep.length < SEARCH_LENGTH.MIN ||
+      normalizedRep.length > SEARCH_LENGTH.MAX
+    )
       return;
@@
       await commonApiFetch<boolean>({
         endpoint: "/rep/categories/availability",
         params: {
-          param: rep,
+          param: normalizedRep,
         },
       });
-      setSelectedCategory(rep);
-      setRepSearch(rep);
+      setSelectedCategory(normalizedRep);
+      setRepSearch(normalizedRep);
@@
   const onSearchSubmit = (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
-    if (!repSearch) return;
-    void onRepSelect(repSearch);
+    const searchValue = repSearch.trim();
+    if (!searchValue) return;
+    void onRepSelect(searchValue);
   };

Also applies to: 201-205

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

In `@components/user/rep/new-rep/UserPageRepNewRepSearch.tsx` around lines 130 -
143, Trim the incoming category string once at the start of onRepSelect (e.g.,
const normalized = rep.trim()), use that normalized value for length validation,
the commonApiFetch params, and when calling setSelectedCategory and setRepSearch
so you never validate or send whitespace-padded values; apply the same
normalization pattern to the other handler that updates/searches the rep (the
one that sets rep search state around lines 201–205) so all validations, API
calls (commonApiFetch), and state updates use the trimmed value consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@components/user/rep/new-rep/UserPageRepNewRepSearch.tsx`:
- Around line 101-110: The render-time state update should be moved into a
useEffect to avoid changing state during render: replace the inline comparison
of repState !== prevRepState with a useEffect that depends on a stable key (e.g.
repState?.id or repState?.rater_contribution) and inside the effect call
setPrevRepState(...) and setAmountStr(`${repState.rater_contribution}`) only
when that key indicates a real category transition; update references to
prevRepState, setPrevRepState, repState, and setAmountStr accordingly and ensure
the effect’s dependency array uses the stable key (or
repState?.rater_contribution) rather than the object identity.
- Around line 130-143: Trim the incoming category string once at the start of
onRepSelect (e.g., const normalized = rep.trim()), use that normalized value for
length validation, the commonApiFetch params, and when calling
setSelectedCategory and setRepSearch so you never validate or send
whitespace-padded values; apply the same normalization pattern to the other
handler that updates/searches the rep (the one that sets rep search state around
lines 201–205) so all validations, API calls (commonApiFetch), and state updates
use the trimmed value consistently.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3b581f and 7e08421.

📒 Files selected for processing (1)
  • components/user/rep/new-rep/UserPageRepNewRepSearch.tsx

Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
@ragnep ragnep merged commit 5b48fd9 into main Feb 27, 2026
6 checks passed
@ragnep ragnep deleted the grant-rep-modal-ux branch February 27, 2026 12:39
@sonarqubecloud
Copy link
Copy Markdown

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