Skip to content

feat: implement Lightning Rounds (Surge Events) — closes #175 - #177

Merged
Benjtalkshow merged 4 commits into
boundlessfi:mainfrom
Sendi0011:feat/lightning-rounds-175
Apr 25, 2026
Merged

Benjtalkshow merged 4 commits into
boundlessfi:mainfrom
Sendi0011:feat/lightning-rounds-175

Conversation

@Sendi0011

@Sendi0011 Sendi0011 commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

feat: implement Lightning Rounds (Surge Events) — closes #175

Summary

Lightning Rounds are time-boxed high-value bounty events. This PR delivers the full feature: banner, dedicated page, card badges, schedule widget, and hook layer — with zero new backend work required.

Key insight: BountyWindowType already exists

The backend already has everything we need:

  • BountyWindowType type with id, name, startDate, endDate, status
  • Every Bounty already carries a bountyWindow relation (already in BountyFieldsFragment)
  • BountyQueryInput.bountyWindowId filter already works

Lightning Rounds are Bounty Windows. We wire the frontend up to data that's already there.


Files changed

Created

File Purpose
hooks/use-lightning-rounds.ts Core hook layer — useLightningRounds, useActiveLightningRound, useLightningRoundBounties. Groups bounties by bountyWindow, computes phase (active/upcoming/ended) and stats client-side.
components/bounty/lightning-round-banner.tsx Hero banner with live countdown timer, stats, progress bar, CTA. Shown on the bounties page when a round is active.
components/bounty/lightning-round-schedule.tsx Sidebar widget listing upcoming/active/past rounds with phase badges and dates.
app/bounty/lightning-round/page.tsx Dedicated round page — header with countdown, bounties grouped by category, stats grid, progress bar, sidebar.

How it works (no backend changes)

activeBounties query
       │
       ▼
useActiveLightningRound()    ← groups by bountyWindow, finds phase === "active"
       │
       ▼
LightningRoundBanner         ← shown on /bounty when round is live

BountyQueryInput.bountyWindowId filter


useLightningRoundBounties(id) ← fetches bounties scoped to one round


/bounty/lightning-round?id= ← dedicated page

Phase (active / upcoming / ended) is computed from startDate and endDate — no extra DB column needed.


Acceptance criteria

  • Active Lightning Rounds display with countdown
  • Bounties associated with rounds have special badges
  • Dedicated round page shows curated bounties by category
  • Round schedule visible for upcoming events
  • Stats tracked per round (participation, completion)

Testing notes

  • If no bounties have a bountyWindow, the banner simply does not render (null-safe throughout)
  • The countdown re-renders every second using setInterval cleaned up on unmount
  • BountyCard detects Lightning Round membership via bounty.bountyWindow — already in the fragment, no extra query
  • Round page falls back to the active round when no ?id= param is supplied

Screenshots

Screenshot 2026-04-23 at 20 33 16

Notes screenshot was taking using mocked data to render banner, not committed

Summary by CodeRabbit

  • New Features
    • Dedicated Lightning Round page with live countdowns, phase-aware header, stats, progress bar, grouped bounty sections, and sidebar schedule/leaderboard.
    • Lightning Round banner on the bounties page highlighting active/upcoming rounds and claim progress.
    • Bounty cards visually marked when part of an active Lightning Round.
    • Lightning Round schedule component listing past, active, and upcoming rounds with phase labels and links.
    • Improved loading, empty and unpublished states with skeletons and notices.

@vercel

vercel Bot commented Apr 23, 2026

Copy link
Copy Markdown

@Sendi0011 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Apr 23, 2026

Copy link
Copy Markdown

@Sendi0011 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 248d61ba-f09d-48e5-91d9-1c90468b02b8

📥 Commits

Reviewing files that changed from the base of the PR and between 1c08fef and f6e6367.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • hooks/use-countdown.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/use-countdown.ts

📝 Walkthrough

Walkthrough

Adds Lightning Rounds: new hooks to aggregate round and bounty data, a client countdown hook, a dedicated Lightning Round page, banner and schedule components, and UI updates to highlight Lightning Round bounties.

Changes

Cohort / File(s) Summary
Round data hooks
hooks/use-lightning-rounds.ts
New LightningRound model, grouping/aggregation, getRoundPhase, getRoundCountdownTarget, and hooks: useActiveLightningRound, useLightningRounds, useLightningRoundBounties.
Countdown hook
hooks/use-countdown.ts
New client hook useCountdown(targetMs) and CountdownTime type using useSyncExternalStore to tick per second and return D/H/M/S or null.
Pages
app/bounty/lightning-round/page.tsx, app/bounty/page.tsx
New client Lightning Round page resolving target round, fetching round bounties, rendering phase-aware header, grouped bounties, loading/empty states; app/bounty/page.tsx now queries active round and conditionally renders LightningRoundBanner.
Banner & Schedule
components/bounty/lightning-round-banner.tsx, components/bounty/lightning-round-schedule.tsx
New LightningRoundBanner (countdown, stats, progress, CTA) and LightningRoundSchedule (round list with phase badges, loading/error/empty states).
Bounty card UI
components/bounty/bounty-card.tsx
Adds Lightning Round detection via getRoundPhase, highlights cards (yellow ring/glow) and prepends a Zap top bar with round/window name for round bounties.
Misc
package.json
Updated npm alias for @creit-tech/stellar-wallets-kit dependency target package name.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Page as Lightning Round Page (client)
    participant Hook as useLightningRoundBounties / useLightningRounds
    participant API as GraphQL Bounty Query
    participant UI as Components (Banner, Cards, Schedule)
    rect rgba(255,200,0,0.5)
    User->>Page: Navigate to /bounty/lightning-round?id=...
    Page->>Page: resolve targetId (query / active / upcoming)
    Page->>Hook: request round + bounties for targetId
    Hook->>API: fetch bounties (window filter / limit)
    API-->>Hook: return bounty data
    Hook->>Hook: group by window/type, compute stats, getRoundPhase
    Hook-->>Page: return round object + groupedByType
    Page->>UI: render Banner (countdown, stats) and grouped BountyCards
    UI->>User: display live countdown (useCountdown), progress, and highlighted cards
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • Implement Lightning Rounds (Surge Events) Feature #175 — Implements Lightning Rounds feature end-to-end (page, hooks, banner, schedule, card badges); aligns with this PR's objectives.
  • #145 — Directly related; describes creating same files/components and integrations for Lightning Rounds.

Possibly related PRs

Poem

⚡ In burrows bright I plot and hop,
Countdowns hum and banners pop,
Zap-lit cards and rounds that race,
I nibble stats and mark my place,
Rabbity cheers — lightning hops non-stop! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% 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 'feat: implement Lightning Rounds (Surge Events) — closes #175' clearly and concisely summarizes the main feature being implemented across the entire changeset.
Linked Issues check ✅ Passed All five acceptance criteria from issue #175 are implemented: active countdown display (hooks/use-countdown.ts, LightningRoundBanner), bounty badges (BountyCard with isLightningRound detection), dedicated round page with category grouping (app/bounty/lightning-round/page.tsx), schedule visibility (LightningRoundSchedule), and per-round stats (LightningRound with stats aggregation).
Out of Scope Changes check ✅ Passed The package.json change (dependency alias update from JSR to npm) is directly scoped to resolving a CI failure blocking the Lightning Rounds implementation and is documented in PR comments as addressing a pre-existing registry issue.

✏️ 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.

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

🧹 Nitpick comments (1)
components/bounty/lightning-round-schedule.tsx (1)

22-47: Relative-time labels are static until the hook refetches.

formatDistanceToNow is evaluated once per render of RoundRow. Unlike the banner's countdown, the schedule has no interval, so "Ends in 2 hours" stays that way while the user sits on the page. For a sidebar that advertises a time-boxed event, this may be OK — but worth wiring a low-frequency (e.g. 60s) useEffect or reusing a shared useNow() if precision matters. Otherwise, consider wording that tolerates staleness better (e.g. absolute end time on hover).

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

In `@components/bounty/lightning-round-schedule.tsx` around lines 22 - 47, The
relative-time string in phaseConfig (computed via formatDistanceToNow for the
"active" dateLine) is only evaluated once per render of RoundRow and thus
becomes stale; fix by updating RoundRow to refresh the relative time
periodically—either reuse a shared hook like useNow() or add a low-frequency
useEffect interval (e.g., 60s) that forces a state tick and causes
phaseConfig/dateLine to recompute (or replace the dateLine with an absolute time
and show relative time on hover); target the code paths that build phaseConfig
and the RoundRow component so the active case updates live.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/bounty/lightning-round/page.tsx`:
- Around line 352-365: The empty-state conditional is too narrow: update the
render condition that currently reads `isError || (!round && !targetId)` to
`isError || !round` so that when a `targetId` is present but the query returns
no matching `round` the empty state is shown; keep the existing loading guard
(`isLoading || listLoading`) intact so this change only affects the non-loading
branch in the component rendering logic where `round`, `isError`, and `targetId`
are used.
- Around line 319-334: The page currently calls useSearchParams() directly
inside the LightningRoundPage component which forces a CSR bailout; wrap the
part of the component that uses useSearchParams() in a React.Suspense boundary
(or move the hook into a client-only subcomponent) so that the route can
preserve static rendering. Concretely, either wrap the JSX that reads
searchParams/windowId with <Suspense fallback={...}>...</Suspense> inside
LightningRoundPage or extract the search-params-dependent logic into a new
client component (e.g., LightningRoundClient) that calls useSearchParams(), then
render that client component from the server component to avoid the CSR opt-out.

In `@components/bounty/lightning-round-banner.tsx`:
- Around line 215-245: The banner shows a "claimed" label while using
claimedCount + completedCount and the numeric percent can render >100; update
the copy and percent guard: change the visible label where you render
{round.stats.claimedCount + round.stats.completedCount} of
{round.stats.totalBounties} to say "claimed or completed" (matching
RoundHeader/RoundHeader usage) and wrap the computed percent displayed text in
Math.min(100, ...) just like the width style uses Math.min to clamp the value;
adjust the percent expression and keep the existing width calculation on the
inner div (referencing isActive and
round.stats.claimedCount/completedCount/totalBounties to locate the code).
- Around line 19-51: useCountdown currently depends on the Date object identity
and keeps the interval running and emitting new zero objects after expiry;
change the effect to depend on a numeric timestamp and stop the timer when
expired: inside useCountdown use targetTs = target?.getTime() as the effect
dependency (instead of target), compute diff from targetTs, and when diff <= 0
call setTimeLeft once to a stable zero value (or null) and clearInterval(id) so
the interval is stopped; also ensure you only setTimeLeft when the computed
values actually differ (or use a ref flag like isExpiredRef) to avoid emitting a
new identical object each second. Apply the same dependency/timer-stop fix to
the duplicate useCountdown implementation in
app/bounty/lightning-round/page.tsx.

In `@hooks/use-lightning-rounds.ts`:
- Around line 84-91: The stats object currently sets currency from
bounty.rewardCurrency when initializing a window entry (stats.currency) and
never reconciles mismatches, causing mixed-currency groups to show an incorrect
single currency; update the logic in the code that creates/updates window
entries (the place that initializes stats and the code paths that add bounties
to a window) to either (A) enforce a single currency by checking
bounty.rewardCurrency on add and skipping or asserting if it differs, or (B)
convert stats.currency into a per-currency totals map (e.g., totalsByCurrency)
and sum totalValue into the appropriate currency bucket, updating all consumers
(banner/header rendering) to use the per-currency totals; modify functions that
reference stats.currency and totalValue to use the chosen approach and reconcile
existing entries when adding new bounties.
- Around line 160-176: The hook useLightningRounds currently hardcodes
useBountiesQuery with limit: 100 which silently truncates rounds; change
useLightningRounds to accept a limit (or full options) param and pass it through
to useBountiesQuery instead of fixed limit:100, or implement pagination inside
useLightningRounds to fetch pages until all distinct bountyWindow ids are
collected (use groupBountiesByWindow to detect new windows) before computing
rounds/activeRound/upcomingRounds/endedRounds (functions to update:
useLightningRounds, the call site of useBountiesQuery, and the grouping logic
using groupBountiesByWindow and getRoundPhase).
- Around line 37-48: The function getRoundPhase currently returns "active" when
both startDate and endDate are missing, causing UI to show live state for
incomplete data; update getRoundPhase to detect when start and end are both
null/undefined (using the existing start and end variables from the
LightningRound pick) and return "upcoming" (or an explicit "unknown" if
preferred) before falling through to other checks so rounds without any dates do
not render as live/active in BountyCard, LightningRoundBanner, or RoundHeader.
- Around line 201-223: The hook useLightningRoundBounties fires useBountiesQuery
even when windowId is empty; update the call to useBountiesQuery to pass an
enabled flag (e.g., enabled: Boolean(windowId) or enabled: windowId !== "") so
the query is disabled when windowId is falsy/empty, keeping the rest of the hook
(groupBountiesByWindow, grouping logic, returned shape) unchanged and ensuring
no unnecessary network request occurs when windowId is not provided.

---

Nitpick comments:
In `@components/bounty/lightning-round-schedule.tsx`:
- Around line 22-47: The relative-time string in phaseConfig (computed via
formatDistanceToNow for the "active" dateLine) is only evaluated once per render
of RoundRow and thus becomes stale; fix by updating RoundRow to refresh the
relative time periodically—either reuse a shared hook like useNow() or add a
low-frequency useEffect interval (e.g., 60s) that forces a state tick and causes
phaseConfig/dateLine to recompute (or replace the dateLine with an absolute time
and show relative time on hover); target the code paths that build phaseConfig
and the RoundRow component so the active case updates live.
🪄 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: CHILL

Plan: Pro

Run ID: a7a94348-41d6-4d0d-a864-a9f856e63cc5

📥 Commits

Reviewing files that changed from the base of the PR and between ed167b6 and 7eb7a55.

📒 Files selected for processing (6)
  • app/bounty/lightning-round/page.tsx
  • app/bounty/page.tsx
  • components/bounty/bounty-card.tsx
  • components/bounty/lightning-round-banner.tsx
  • components/bounty/lightning-round-schedule.tsx
  • hooks/use-lightning-rounds.ts

Comment thread app/bounty/lightning-round/page.tsx Outdated
Comment thread app/bounty/lightning-round/page.tsx Outdated
Comment on lines +352 to +365
{isLoading || listLoading ? (
<PageSkeleton />
) : isError || (!round && !targetId) ? (
<div className="flex flex-col items-center justify-center py-32 text-center">
<Zap className="size-12 text-muted-foreground mb-4" />
<h2 className="text-xl font-bold mb-2">No Lightning Round Found</h2>
<p className="text-muted-foreground mb-6">
There are no active or upcoming Lightning Rounds right now.
</p>
<Button asChild variant="outline">
<Link href="/bounty">Browse All Bounties</Link>
</Button>
</div>
) : round ? (

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

Empty-state condition undercounts.

isError || (!round && !targetId) only shows the empty state when there is no target id. If targetId is present (e.g. user arrived with ?id=<stale>) but the query returns no matching round, isError is false and round is null → falls through to null (line 402) and the user sees a blank page below the back-nav. Consider isError || !round (gated on !isLoading && !listLoading, already handled above) to cover "id points at nothing" too.

♻️ Suggested fix
-        ) : isError || (!round && !targetId) ? (
+        ) : isError || !round ? (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isLoading || listLoading ? (
<PageSkeleton />
) : isError || (!round && !targetId) ? (
<div className="flex flex-col items-center justify-center py-32 text-center">
<Zap className="size-12 text-muted-foreground mb-4" />
<h2 className="text-xl font-bold mb-2">No Lightning Round Found</h2>
<p className="text-muted-foreground mb-6">
There are no active or upcoming Lightning Rounds right now.
</p>
<Button asChild variant="outline">
<Link href="/bounty">Browse All Bounties</Link>
</Button>
</div>
) : round ? (
{isLoading || listLoading ? (
<PageSkeleton />
) : isError || !round ? (
<div className="flex flex-col items-center justify-center py-32 text-center">
<Zap className="size-12 text-muted-foreground mb-4" />
<h2 className="text-xl font-bold mb-2">No Lightning Round Found</h2>
<p className="text-muted-foreground mb-6">
There are no active or upcoming Lightning Rounds right now.
</p>
<Button asChild variant="outline">
<Link href="/bounty">Browse All Bounties</Link>
</Button>
</div>
) : round ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/bounty/lightning-round/page.tsx` around lines 352 - 365, The empty-state
conditional is too narrow: update the render condition that currently reads
`isError || (!round && !targetId)` to `isError || !round` so that when a
`targetId` is present but the query returns no matching `round` the empty state
is shown; keep the existing loading guard (`isLoading || listLoading`) intact so
this change only affects the non-loading branch in the component rendering logic
where `round`, `isError`, and `targetId` are used.

Comment thread components/bounty/lightning-round-banner.tsx Outdated
Comment thread components/bounty/lightning-round-banner.tsx
Comment thread hooks/use-lightning-rounds.ts
Comment thread hooks/use-lightning-rounds.ts
Comment thread hooks/use-lightning-rounds.ts Outdated
Comment thread hooks/use-lightning-rounds.ts Outdated

@Benjtalkshow Benjtalkshow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey @Sendi0011, solid first pass and the "no new backend work" framing is correct. Reusing BountyWindowType is the right call. A few things to flag before this can merge.

useCountdown is duplicated verbatim between components/bounty/lightning-round-banner.tsx and app/bounty/lightning-round/page.tsx. Please move it into hooks/use-lightning-rounds.ts (or a small use-countdown.ts) so both consumers import the same implementation, otherwise any fix to one copy silently misses the other.

useLightningRounds fetches up to 100 bounties just to derive the schedule sidebar widget, every time it mounts. If the app ever has more than 100 active-ish bounties, older rounds silently drop off the list, and even when it doesn't, that's a lot of data for a 3-item sidebar. Either a dedicated "list bounty windows" query or a small server-derived digest would be cheaper.

Within a phase, rounds come back in insertion order (whatever order the bounty list returned them). Upcoming rounds should probably be sorted ascending by startDate and ended rounds descending by endDate so the schedule widget reads naturally. Right now two upcoming rounds can swap order depending on which of their bounties was created first.

In useLightningRoundBounties, you pull round from groupBountiesByWindow(bounties)[0]. If the backend ever returns bounties whose bountyWindow is null even when bountyWindowId was filtered (stale cache, partial fragment, whatever), round ends up null and the page renders nothing under the back-nav even though groupedByType has data. Worth falling back to rendering the category sections without the header instead of a blank screen.

The Lightning Round styling on BountyCard only kicks in when the round phase is "active", so a bounty from an upcoming round looks identical to a regular one. If the goal is to advertise the round as soon as the bounty appears, include "upcoming" in the check. If the goal is only "claimable right now", leave it, but worth mentioning in the PR body so design can weigh in.

CI/CD fails because pnpm-lock.yaml is out of sync with package.json after the main merge. Run pnpm install and commit the updated lockfile.

Please also address all of CodeRabbit's corrections (Suspense boundary around useSearchParams, the target identity in useCountdown, the empty-state condition, the phase default, and the currency-from-first-bounty note are all valid).

Nice work overall. Ping me once these are in and I'll take another pass.

@Sendi0011

Copy link
Copy Markdown
Contributor Author

GM @Benjtalkshow , thanks for the detailed feedback — going through everything now. The CodeRabbit flags on the countdown interval and the currency aggregation are valid catches, will clean those up along with the rest.
will work and push the fixes soon

- extract useCountdown into hooks/use-countdown.ts (dedup, stable dep, auto-stop)
- fix getRoundPhase: missing dates → upcoming not active
- fix currency: per-currency totals, primaryCurrency by highest value
- fix within-phase sort: upcoming asc startDate, ended desc endDate
- useLightningRounds: parameterise limit (default 200), document tradeoff
- useLightningRoundBounties: disable query when windowId is falsy
- lightning-round/page: wrap useSearchParams in Suspense boundary
- fix empty-state: isError || (!round && categories.length === 0)
- RoundHeader optional: render category sections even when round is null

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

🧹 Nitpick comments (1)
hooks/use-lightning-rounds.ts (1)

240-243: Minor: ...params spread can silently override the explicit limit argument.

{ limit, sortBy: "createdAt", sortOrder: "desc", ...params } lets a caller passing params.limit override the second positional limit argument, which is probably surprising given the dedicated parameter. If the intent is for the explicit limit arg to win (typical), spread params first. Otherwise document that params.limit takes precedence.

♻️ Suggested change
-  const { data, isLoading, isError, error, refetch } = useBountiesQuery({
-    query: { limit, sortBy: "createdAt", sortOrder: "desc", ...params },
-  });
+  const { data, isLoading, isError, error, refetch } = useBountiesQuery({
+    query: { sortBy: "createdAt", sortOrder: "desc", ...params, limit },
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-lightning-rounds.ts` around lines 240 - 243, The current object
passed to useBountiesQuery in useLightningRounds spreads ...params after {
limit, sortBy, sortOrder } which allows params.limit to override the explicit
limit argument; change the merge order so params is spread first (e.g., {
...params, limit, sortBy: "createdAt", sortOrder: "desc" }) so the function's
limit parameter wins, updating the call site in useLightningRounds where
useBountiesQuery is invoked; alternatively, if you intend params to win, add a
comment/docstring on useLightningRounds stating that params.limit takes
precedence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hooks/use-countdown.ts`:
- Around line 41-62: The subscribe function passed to useSyncExternalStore (in
useCountdown) must be memoized and the snapshot must be cached to avoid unstable
identities: create a snapshotRef via useRef keyed by targetMs and update it in a
stable getSnapshot implementation that returns snapshotRef.current (computing
and writing compute(targetMs) only when targetMs changed), and wrap the
subscribe callback (the function that sets/clears setInterval and calls
onStoreChange) in useCallback so its identity is stable across renders; refer to
useCountdown, getSnapshot, subscribe, compute, and snapshotRef when making these
changes.

---

Nitpick comments:
In `@hooks/use-lightning-rounds.ts`:
- Around line 240-243: The current object passed to useBountiesQuery in
useLightningRounds spreads ...params after { limit, sortBy, sortOrder } which
allows params.limit to override the explicit limit argument; change the merge
order so params is spread first (e.g., { ...params, limit, sortBy: "createdAt",
sortOrder: "desc" }) so the function's limit parameter wins, updating the call
site in useLightningRounds where useBountiesQuery is invoked; alternatively, if
you intend params to win, add a comment/docstring on useLightningRounds stating
that params.limit takes precedence.
🪄 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: CHILL

Plan: Pro

Run ID: ba0356a7-0503-401b-b354-f32e9de9041d

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb7a55 and 1c08fef.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • app/bounty/lightning-round/page.tsx
  • components/bounty/lightning-round-banner.tsx
  • hooks/use-countdown.ts
  • hooks/use-lightning-rounds.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/bounty/lightning-round/page.tsx

Comment thread hooks/use-countdown.ts
@Sendi0011

Copy link
Copy Markdown
Contributor Author

GM @Benjtalkshow, thanks for the thorough review. Here's what I've addressed:

  1. Extracted useCountdown into its own hooks/use-countdown.ts — both the banner and round page now import from the same place.

  2. Raised the limit to 200 and made it a named param on useLightningRounds with a comment about the tradeoff. Agreed a dedicated bountyWindows query would be the cleaner long-term fix.

  3. Within-phase sorting is in — upcoming rounds sort by ascending startDate, ended by descending endDate, active by ascending endDate (soonest to expire first).

  4. RoundHeader is now rendered conditionally so category sections still show even when the bounty window relation comes back incomplete.

  5. On the badge — I left it active-only for now since the intent was to signal "claimable right now" rather than just round membership. Happy to include upcoming if you'd prefer it advertises the round earlier, just let me know.

  6. Ran pnpm install and committed the updated lockfile.

All CodeRabbit comments (Suspense boundary, interval teardown, empty-state condition, phase default, currency aggregation, progress label consistency) are addressed too.

@Sendi0011

Copy link
Copy Markdown
Contributor Author

And also, the CI failure is a pre-existing issue unrelated to this PR — @jsr/creit-tech__stellar-wallets-kit is returning a 404 from npm.jsr.io during pnpm install --frozen-lockfile. Looks like it's either been unpublished or the JSR registry needs auth configured in the CI environment. Happy to help track it down but it's not caused by any of my changes.

pnpm constructs JSR tarball URLs as `/-/pkg-version.tgz` while JSR
actually serves them at `/~/<bucket>/.../version.tgz`, causing
`pnpm install --frozen-lockfile` to 404 in CI (and locally) on every
PR. The same package is published on npm under `@creit.tech/...`
(with a dot) by the same publisher, so swap the alias to point at
npm and regenerate the lockfile. Resolves boundlessfi#187.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@Benjtalkshow Benjtalkshow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@Benjtalkshow
Benjtalkshow merged commit 31835c4 into boundlessfi:main Apr 25, 2026
2 of 3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 25, 2026
7 tasks
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.

Implement Lightning Rounds (Surge Events) Feature

2 participants