Skip to content

feat(frontend): live countdown timer + search bar for bounties#887

Open
TeapoyY wants to merge 5 commits intoSolFoundry:mainfrom
TeapoyY:feature/bounty-countdown-timer
Open

feat(frontend): live countdown timer + search bar for bounties#887
TeapoyY wants to merge 5 commits intoSolFoundry:mainfrom
TeapoyY:feature/bounty-countdown-timer

Conversation

@TeapoyY
Copy link
Copy Markdown

@TeapoyY TeapoyY commented Apr 4, 2026

Summary

Implements multiple T1 bounties from SolFoundry/solfoundry:

Bounty T1: Bounty Countdown Timer (#826)

  • New component: src/components/bounty/BountyCountdown.tsx
    • Real-time ticking countdown showing days, hours, minutes, seconds
    • Color changes to warning (<24h) and urgent (<1h)
    • Shows "Expired" when deadline passes
    • Works as both inline and badge variants
    • Uses useEffect + setInterval for live updates (1s interval)
  • Integrated into BountyCard (replacing static timeLeft call) and BountyDetail sidebar

Bounty T1: Add Search Bar to Bounties Page (#823)

  • Added debounced (300ms) search input to BountyGrid
  • Filters client-side across: title, description, skills, category, org/repo name
  • Clear button (X) to reset search
  • Result count shown above grid when searching
  • Empty state with link to clear search
  • Works alongside existing status and language filters

Bounty T1: Mobile Responsive Polish (#824)

  • Added global overflow-x: hidden to html/body to prevent horizontal scroll on all pages
  • Fixed Footer contract address container with min-w-0 and flex-1 truncate to prevent overflow
  • Reduced terminal card font size on mobile (text-xs sm:text-sm)
  • All pages remain functional at 375px and 768px breakpoints

Also included (blocking build)

  • src/lib/utils.ts - created missing utilities: timeLeft, formatCurrency, LANG_COLORS, timeAgo
  • src/lib/animations.ts - created missing framer-motion variants: fadeIn, cardHover, pageTransition, staggerContainer, staggerItem, buttonHover, slideInRight

Both were imported by existing files but missing from the repo, causing TypeScript errors.

Acceptance Criteria

  • Timer displays on bounty cards and detail page
  • Updates without page refresh (real-time, every second)
  • Visual urgency indicators (warning <24h, urgent <1h, expired)
  • Search bar visible on /bounties page
  • Typing filters bounties in real-time (debounced 300ms)
  • Works alongside existing filters
  • All pages look correct at 375px width
  • All pages look correct at 768px width
  • No horizontal scroll on any page

- 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
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new BountyCountdown component that encapsulates real-time countdown display logic for bounty deadlines, replacing inline rendering in BountyCard and BountyDetail. It adds client-side search functionality to BountyGrid with debouncing, filtering across multiple bounty fields, and dedicated empty-state handling. New utility functions are provided for deadline formatting (timeLeft, timeAgo) and currency formatting (formatCurrency), along with a set of reusable framer-motion animation variants. Pagination is disabled during active search to prevent conflicting data fetches.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

Possibly related PRs

Suggested labels

approved

Suggested reviewers

  • chronoeth-creator
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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
Title check ✅ Passed The title clearly summarizes the two main features: a live countdown timer and search bar for bounties, matching the changeset's primary additions.
Description check ✅ Passed The pull request description clearly outlines the implementation of multiple T1 bounties with detailed feature descriptions and acceptance criteria that align with the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between f418700 and fa4f61b.

📒 Files selected for processing (6)
  • frontend/src/components/bounty/BountyCard.tsx
  • frontend/src/components/bounty/BountyCountdown.tsx
  • frontend/src/components/bounty/BountyDetail.tsx
  • frontend/src/components/bounty/BountyGrid.tsx
  • frontend/src/lib/animations.ts
  • frontend/src/lib/utils.ts

Comment on lines +32 to +39
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>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +35 to +49
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]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +82 to +101
{/* 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

TeapoyY and others added 4 commits April 7, 2026 12:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-wallet PR is missing a Solana wallet for bounty payout

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant