feat(frontend): live countdown timer + search bar for bounties#887
feat(frontend): live countdown timer + search bar for bounties#887TeapoyY wants to merge 5 commits intoSolFoundry:mainfrom
Conversation
- Add BountyCountdown component with real-time updates (days/hours/minutes/seconds) - Color urgency indicators: warning (<24h), urgent (<1h), expired - Integrate countdown into BountyCard and BountyDetail sidebar - Add debounced search bar to BountyGrid (searches title, description, skills, repo) - Add clear button to reset search - Add empty state for search with no results - Create missing src/lib/utils.ts with timeLeft, formatCurrency, LANG_COLORS, timeAgo - Create missing src/lib/animations.ts with framer-motion variants
📝 WalkthroughWalkthroughThis PR introduces a new Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/bounty/BountyCountdown.tsx`:
- Around line 32-39: The expired branch in BountyCountdown returns early and
bypasses the variant handling, so expired states always render plain inline
markup; update BountyCountdown to respect the variant prop when expired by
moving the expired check into the variant branch or by conditionalizing the
badge rendering (i.e., when expired && variant === "badge" render the same badge
structure and classes as the non-expired badge branch, including using className
and the Clock icon) so callers passing variant="badge" get the badge markup even
after expiration.
In `@frontend/src/components/bounty/BountyGrid.tsx`:
- Around line 82-101: The search input in BountyGrid.tsx (the input bound to
searchQuery / setSearchQuery) lacks an accessible name; add one by giving the
input an id and associating a visible or visually-hidden <label> (using htmlFor)
or by adding a clear aria-label/aria-labelledby attribute (e.g.,
aria-label="Search bounties by title, description, or skill") so screen readers
receive a stable name; ensure the chosen approach matches the existing clear
button behavior and keeps the placeholder unchanged.
- Around line 35-49: The client-side filter (filteredBounties built from
allBounties) only searches already-fetched pages, so searching misses matches on
later pages and the displayed count/Load More behavior is wrong; change to
perform server-side search by passing debouncedSearch into the useBounties hook
(update the hook to accept a query param and return filtered pages and server
totalCount), then replace the client-only filtering (filteredBounties) with the
server-returned list, keep the "Load More" control functional during searches
(so more filtered pages can be fetched), and use the server totalCount for the
result count display instead of counting allBounties. Ensure you update
references to allBounties, filteredBounties, debouncedSearch, useBounties, and
the Load More/result count UI to use the new server-driven data.
- Line 25: The UI and filtering are inconsistent because filtering uses
debouncedSearch while empty states, result count, and pagination toggle still
read raw searchQuery; fix by deriving a single effective search string from the
debounced value (e.g., const effectiveSearch = debouncedSearch.trim()) and use
that everywhere the component decides "search mode" (filtering logic,
empty-state checks, result count display, and pagination toggle) so
whitespace-only input is treated as empty and the UI always reflects the dataset
actually rendered; update all places referencing searchQuery for these concerns
to use effectiveSearch instead.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bc151fc3-74b4-42b2-ae52-25f083cc1653
📒 Files selected for processing (6)
frontend/src/components/bounty/BountyCard.tsxfrontend/src/components/bounty/BountyCountdown.tsxfrontend/src/components/bounty/BountyDetail.tsxfrontend/src/components/bounty/BountyGrid.tsxfrontend/src/lib/animations.tsfrontend/src/lib/utils.ts
| if (expired) { | ||
| return ( | ||
| <span className={`inline-flex items-center gap-1 text-status-error font-medium ${className}`}> | ||
| <Clock className="w-3.5 h-3.5" /> | ||
| Expired | ||
| </span> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Expired deadlines ignore the component’s badge variant.
Line 32 returns before the variant-specific branch on Lines 47-55 runs. Once a deadline expires, any caller that passes variant="badge" falls back to plain inline markup, so the new component only honors its public variant contract for non-expired states.
As per coding guidelines, frontend/**: React/TypeScript frontend. Check: Component structure and state management; Integration with existing components.
Also applies to: 47-55
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/bounty/BountyCountdown.tsx` around lines 32 - 39, The
expired branch in BountyCountdown returns early and bypasses the variant
handling, so expired states always render plain inline markup; update
BountyCountdown to respect the variant prop when expired by moving the expired
check into the variant branch or by conditionalizing the badge rendering (i.e.,
when expired && variant === "badge" render the same badge structure and classes
as the non-expired badge branch, including using className and the Clock icon)
so callers passing variant="badge" get the badge markup even after expiration.
| const allBounties = data?.pages.flatMap((p) => p.items) ?? []; | ||
|
|
||
| // Client-side search filter across title, description, and skills | ||
| const filteredBounties = useMemo(() => { | ||
| if (!debouncedSearch.trim()) return allBounties; | ||
| const q = debouncedSearch.toLowerCase(); | ||
| return allBounties.filter( | ||
| (b) => | ||
| b.title.toLowerCase().includes(q) || | ||
| b.description.toLowerCase().includes(q) || | ||
| b.skills?.some((s) => s.toLowerCase().includes(q)) || | ||
| (b.category ?? '').toLowerCase().includes(q) || | ||
| `${b.org_name ?? ''}/${b.repo_name ?? ''}`.toLowerCase().includes(q) | ||
| ); | ||
| }, [allBounties, debouncedSearch]); |
There was a problem hiding this comment.
Search only covers the pages that have already been fetched.
Line 35 builds allBounties from the pages currently in memory, and frontend/src/hooks/useBounties.ts:13-26 shows that hook paginates in batches of 12. Lines 39-48 therefore search only a partial dataset, while Line 191 hides Load More as soon as a query exists. That means matches on later pages become invisible, and the result count on Lines 169-172 is only a count over the loaded subset, not over all matching bounties.
As per coding guidelines, frontend/**: React/TypeScript frontend. Check: Component structure and state management; Integration with existing components; Error/loading/empty state handling.
Also applies to: 166-172, 191-191
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/bounty/BountyGrid.tsx` around lines 35 - 49, The
client-side filter (filteredBounties built from allBounties) only searches
already-fetched pages, so searching misses matches on later pages and the
displayed count/Load More behavior is wrong; change to perform server-side
search by passing debouncedSearch into the useBounties hook (update the hook to
accept a query param and return filtered pages and server totalCount), then
replace the client-only filtering (filteredBounties) with the server-returned
list, keep the "Load More" control functional during searches (so more filtered
pages can be fetched), and use the server totalCount for the result count
display instead of counting allBounties. Ensure you update references to
allBounties, filteredBounties, debouncedSearch, useBounties, and the Load
More/result count UI to use the new server-driven data.
| {/* Search bar */} | ||
| <div className="relative mb-6"> | ||
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" /> | ||
| <input | ||
| type="text" | ||
| value={searchQuery} | ||
| onChange={(e) => setSearchQuery(e.target.value)} | ||
| placeholder="Search bounties by title, description, or skill..." | ||
| className="w-full bg-forge-800 border border-border rounded-lg pl-10 pr-10 py-2.5 text-sm text-text-primary placeholder:text-text-muted focus:border-emerald outline-none transition-colors duration-150" | ||
| /> | ||
| {searchQuery && ( | ||
| <button | ||
| onClick={() => setSearchQuery('')} | ||
| className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary transition-colors" | ||
| aria-label="Clear search" | ||
| > | ||
| <X className="w-4 h-4" /> | ||
| </button> | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
The new search field has no accessible name.
Lines 85-90 render the input without a <label>, aria-label, or aria-labelledby. Placeholder text is not a stable accessible name, so screen reader users will not get the purpose of the primary new control.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/bounty/BountyGrid.tsx` around lines 82 - 101, The
search input in BountyGrid.tsx (the input bound to searchQuery / setSearchQuery)
lacks an accessible name; add one by giving the input an id and associating a
visible or visually-hidden <label> (using htmlFor) or by adding a clear
aria-label/aria-labelledby attribute (e.g., aria-label="Search bounties by
title, description, or skill") so screen readers receive a stable name; ensure
the chosen approach matches the existing clear button behavior and keeps the
placeholder unchanged.
BountyCountdown: fix expired badge variant — when deadline is expired and variant='badge', still render the badge-style markup instead of returning early with inline markup BountyGrid: refactor useDebounce hook to use useRef for timer, expose cancel() for immediate reset; clear button now cancels pending debounce and resets debounced value immediately BountyGrid: add aria-label to search input for screen readers BountyGrid: add !isError guard to grid render condition to prevent contradictory error+grid display with cached data utils: timeLeft guard against invalid/null deadline strings (NaN) utils: timeAgo handle invalid dates and future timestamps utils: formatCurrency preserve up to 2 decimal places
…ssibility and consistency - Add JSDoc comments to all exported components and their props interfaces (BountyCard, BountyCountdown, BountyDetail, SubmissionForm, BountyCreateWizard, helper components) - Fix BountyCountdown: check variant === 'badge' before expired to render badge structure with Clock icon when expired && badge, rather than bypassing variant handling - Verify aria-label already present on search input for accessibility - Confirm effectiveSearch (debouncedSearch.trim()) is used consistently for all search-mode logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Implements multiple T1 bounties from SolFoundry/solfoundry:
Bounty T1: Bounty Countdown Timer (#826)
src/components/bounty/BountyCountdown.tsxBounty T1: Add Search Bar to Bounties Page (#823)
Bounty T1: Mobile Responsive Polish (#824)
overflow-x: hiddento html/body to prevent horizontal scroll on all pagesmin-w-0andflex-1 truncateto prevent overflowtext-xs sm:text-sm)Also included (blocking build)
src/lib/utils.ts- created missing utilities: timeLeft, formatCurrency, LANG_COLORS, timeAgosrc/lib/animations.ts- created missing framer-motion variants: fadeIn, cardHover, pageTransition, staggerContainer, staggerItem, buttonHover, slideInRightBoth were imported by existing files but missing from the repo, causing TypeScript errors.
Acceptance Criteria