feat: add shared compact stats and reward trend cards#858
Conversation
📝 WalkthroughWalkthroughThis PR adds mini reward trend sparkline cards and expandable compact stat groups to the GameLobby and ProfileSettings pages. GameLobby derives stats from game activity and prize signals, while ProfileSettings derives stats from wallet connection and profile metadata. Both pages include responsive CSS grids and test coverage for loading and empty states. ChangesReward Trend and Compact Stats Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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: 2
🧹 Nitpick comments (1)
frontend/src/pages/ProfileSettings.tsx (1)
314-322: ⚡ Quick winPrefer CSS class over inline styles for consistency.
The reward trend grid uses inline styles, while
GameLobby.tsxuses the.lobby-reward-trend-gridCSS class for similar layout. Extract these inline styles to a shared or page-specific CSS class to improve maintainability and consistency.♻️ Proposed refactor
In a CSS file (e.g.,
ProfileSettings.css):.profile-reward-trend-grid { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin-bottom: 1rem; }Then replace the inline styles:
- <div - data-testid="profile-settings-reward-trend-grid" - style={{ - display: 'grid', - gap: '0.75rem', - gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', - marginBottom: '1rem', - }} - > + <div + data-testid="profile-settings-reward-trend-grid" + className="profile-reward-trend-grid" + >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/pages/ProfileSettings.tsx` around lines 314 - 322, The reward trend grid in ProfileSettings (the div with data-testid="profile-settings-reward-trend-grid") is using inline styles instead of a CSS class; extract the inline style block (display, gap, gridTemplateColumns, marginBottom) into a CSS class (e.g., .profile-reward-trend-grid) in a shared or page-specific stylesheet and replace the inline style with className="profile-reward-trend-grid" in the ProfileSettings component to match the existing pattern used by GameLobby.tsx (.lobby-reward-trend-grid) for consistency and maintainability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/pages/GameLobby.tsx`:
- Around line 687-690: rewardTrendCards is currently setting change="0%" and
trend="up" when activeGamesDelta === null, which conflicts with
compactLobbyStats where null maps to undefined (or a "flat" semantic); update
the mapping in rewardTrendCards (the expressions that set change and trend
around activeGamesDelta) so that when activeGamesDelta === null you set trend to
undefined (or "flat") and change to undefined (instead of "0%"), and keep the
existing signed/min calculation only for non-null values (use the same ternary
guards as compactLobbyStats to ensure consistent behavior).
In `@frontend/tests/components/ProfileSettings.test.tsx`:
- Around line 100-123: The test is asserting for testId
"profile-settings-reward-bonus-empty" but ProfileSettings.tsx always renders
testId "profile-settings-reward-bonus" (see ProfileSettings component and its
use of RewardBalanceSparklineCard); either update the test to expect
"profile-settings-reward-bonus" or change ProfileSettings.tsx to conditionally
render the empty variant (e.g., append "-empty" to the testId when
walletStatusState.capabilities.isConnected is false or when
RewardBalanceSparklineCard would render an empty state) so the testId in the
rendered output matches the test assertion.
---
Nitpick comments:
In `@frontend/src/pages/ProfileSettings.tsx`:
- Around line 314-322: The reward trend grid in ProfileSettings (the div with
data-testid="profile-settings-reward-trend-grid") is using inline styles instead
of a CSS class; extract the inline style block (display, gap,
gridTemplateColumns, marginBottom) into a CSS class (e.g.,
.profile-reward-trend-grid) in a shared or page-specific stylesheet and replace
the inline style with className="profile-reward-trend-grid" in the
ProfileSettings component to match the existing pattern used by GameLobby.tsx
(.lobby-reward-trend-grid) for consistency and maintainability.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 31265bec-2743-411f-a13f-f96cfd6740ec
📒 Files selected for processing (5)
frontend/src/pages/GameLobby.tsxfrontend/src/pages/GameLobbyDashboard.cssfrontend/src/pages/ProfileSettings.tsxfrontend/tests/components/GameLobby.test.tsxfrontend/tests/components/ProfileSettings.test.tsx
| activeGamesDelta === null | ||
| ? "0%" | ||
| : `${activeGamesDelta > 0 ? "+" : ""}${Math.min(Math.abs(activeGamesDelta) * 5, 25)}%`, | ||
| trend: activeGamesDelta !== null && activeGamesDelta < 0 ? ("down" as const) : ("up" as const), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for trend calculations to verify null-delta handling patterns.
rg -nP --type=ts --type=tsx -C3 'trend.*:.*activeGamesDelta'Repository: theblockcade/stellarcade
Length of output: 96
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant region in GameLobby.tsx
FILE="frontend/src/pages/GameLobby.tsx"
echo "=== Context around rewardTrendCards (lines ~660-720) ==="
nl -ba "$FILE" | sed -n '640,720p'
echo "=== Context around compactLobbyStats (search for compactLobbyStats) ==="
rg -n "compactLobbyStats" "$FILE"
# Print a window around the first match (if any)
MATCH_LINE=$(rg -n "compactLobbyStats" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${MATCH_LINE:-}" ]; then
START=$((MATCH_LINE-30))
END=$((MATCH_LINE+30))
nl -ba "$FILE" | sed -n "${START},${END}p"
fi
echo "=== Find all uses of activeGamesDelta in this file ==="
rg -n "activeGamesDelta" "$FILE" --heading
echo "=== Search for trend assignments involving activeGamesDelta across TS/TSX ==="
rg -n --glob='*.ts' --glob='*.tsx' -C3 "trend.*activeGamesDelta|activeGamesDelta.*trend" .Repository: theblockcade/stellarcade
Length of output: 186
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="frontend/src/pages/GameLobby.tsx"
echo "=== Check file exists ==="
ls -l "$FILE"
echo "=== rewardTrendCards region (~630-720) ==="
awk 'NR>=630 && NR<=720 {printf "%d\t%s\n", NR, $0}' "$FILE"
echo "=== Find compactLobbyStats occurrences (with context) ==="
rg -n "compactLobbyStats" "$FILE" -C2
echo "=== compactLobbyStats region around first occurrence(s) ==="
# Print a generous window around each match line
for L in $(rg -n "compactLobbyStats" "$FILE" | cut -d: -f1); do
START=$((L-80)); END=$((L+80))
awk -v s="$START" -v e="$END" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n "1,120p"
echo "----"
done
echo "=== Find all assignments/uses of activeGamesDelta in this file ==="
rg -n "activeGamesDelta" "$FILE" --headingRepository: theblockcade/stellarcade
Length of output: 19333
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for null-delta handling for activeGamesDelta ==="
rg -n --glob='*.ts' --glob='*.tsx' "activeGamesDelta === null|activeGamesDelta !== null" .
echo "=== Search for trend assignments that involve activeGamesDelta ==="
rg -n --glob='*.ts' --glob='*.tsx' -C3 "trend[^\\n]*activeGamesDelta|trend[^\\n]*activeGamesDelta[^\\n]*:|:.*activeGamesDelta[^\\n]*trend" .
echo "=== Search for specific rewardTrendCards change/trend logic patterns ==="
rg -n --glob='*.ts' --glob='*.tsx' -C3 "activeGamesDelta.*< 0.*\\(\"down\" as const\\)|(\"down\" as const).*activeGamesDelta" .Repository: theblockcade/stellarcade
Length of output: 2478
Fix prize-reserve trend when activeGamesDelta is null
rewardTrendCards sets change to "0%" when activeGamesDelta === null, but forces trend to "up"; meanwhile compactLobbyStats sets trend to undefined (and change to undefined) for the same activeGamesDelta === null case. Consider mapping activeGamesDelta === null to "flat" or undefined in rewardTrendCards to match the existing behavior in compactLobbyStats (e.g., around GameLobby.tsx lines 687-690 vs 628-633).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/pages/GameLobby.tsx` around lines 687 - 690, rewardTrendCards is
currently setting change="0%" and trend="up" when activeGamesDelta === null,
which conflicts with compactLobbyStats where null maps to undefined (or a "flat"
semantic); update the mapping in rewardTrendCards (the expressions that set
change and trend around activeGamesDelta) so that when activeGamesDelta === null
you set trend to undefined (or "flat") and change to undefined (instead of
"0%"), and keep the existing signed/min calculation only for non-null values
(use the same ternary guards as compactLobbyStats to ensure consistent
behavior).
| it('renders mini reward trend cards with empty fallback when wallet is disconnected', async () => { | ||
| mockGetProfile.mockResolvedValueOnce({ | ||
| success: true, | ||
| data: { | ||
| address: 'GABC123', | ||
| username: 'alice', | ||
| createdAt: '2025-01-01T12:00:00Z', | ||
| }, | ||
| }); | ||
|
|
||
| walletStatusState.address = null; | ||
| walletStatusState.network = null; | ||
| walletStatusState.provider = null; | ||
| walletStatusState.capabilities = { isConnected: false }; | ||
| walletStatusState.status = 'disconnected'; | ||
| walletStatusState.lastUpdatedAt = null; | ||
|
|
||
| render(<ProfileSettings />); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByTestId('profile-settings-reward-earned')).toBeInTheDocument(); | ||
| expect(screen.getByTestId('profile-settings-reward-bonus-empty')).toBeInTheDocument(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Test assertion expects a testId that is not rendered.
Line 121 expects profile-settings-reward-bonus-empty, but ProfileSettings.tsx line 345 always renders testId="profile-settings-reward-bonus" regardless of wallet connection state. Unless RewardBalanceSparklineCard internally modifies the testId when rendering an empty state, this assertion will fail.
🐛 Proposed fix
If RewardBalanceSparklineCard does not append -empty to the testId, update the test assertion:
await waitFor(() => {
expect(screen.getByTestId('profile-settings-reward-earned')).toBeInTheDocument();
- expect(screen.getByTestId('profile-settings-reward-bonus-empty')).toBeInTheDocument();
+ expect(screen.getByTestId('profile-settings-reward-bonus')).toBeInTheDocument();
});Alternatively, if the component should distinguish empty states in its testId, update ProfileSettings.tsx to conditionally append -empty:
<RewardBalanceSparklineCard
label="Bonus momentum"
status={rewardTrendStatus}
error={error ?? undefined}
onRetry={() => undefined}
balance={walletMeta.connected ? 'Ready' : undefined}
balanceEquivalent={walletMeta.connected ? walletMeta.network : undefined}
dataPoints={bonusRewardSeries}
change={walletMeta.connected ? '+3%' : '0%'}
trend={walletMeta.connected ? 'up' : 'down'}
- testId="profile-settings-reward-bonus"
+ testId={`profile-settings-reward-bonus${walletMeta.connected ? '' : '-empty'}`}
/>📝 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.
| it('renders mini reward trend cards with empty fallback when wallet is disconnected', async () => { | |
| mockGetProfile.mockResolvedValueOnce({ | |
| success: true, | |
| data: { | |
| address: 'GABC123', | |
| username: 'alice', | |
| createdAt: '2025-01-01T12:00:00Z', | |
| }, | |
| }); | |
| walletStatusState.address = null; | |
| walletStatusState.network = null; | |
| walletStatusState.provider = null; | |
| walletStatusState.capabilities = { isConnected: false }; | |
| walletStatusState.status = 'disconnected'; | |
| walletStatusState.lastUpdatedAt = null; | |
| render(<ProfileSettings />); | |
| await waitFor(() => { | |
| expect(screen.getByTestId('profile-settings-reward-earned')).toBeInTheDocument(); | |
| expect(screen.getByTestId('profile-settings-reward-bonus-empty')).toBeInTheDocument(); | |
| }); | |
| }); | |
| it('renders mini reward trend cards with empty fallback when wallet is disconnected', async () => { | |
| mockGetProfile.mockResolvedValueOnce({ | |
| success: true, | |
| data: { | |
| address: 'GABC123', | |
| username: 'alice', | |
| createdAt: '2025-01-01T12:00:00Z', | |
| }, | |
| }); | |
| walletStatusState.address = null; | |
| walletStatusState.network = null; | |
| walletStatusState.provider = null; | |
| walletStatusState.capabilities = { isConnected: false }; | |
| walletStatusState.status = 'disconnected'; | |
| walletStatusState.lastUpdatedAt = null; | |
| render(<ProfileSettings />); | |
| await waitFor(() => { | |
| expect(screen.getByTestId('profile-settings-reward-earned')).toBeInTheDocument(); | |
| expect(screen.getByTestId('profile-settings-reward-bonus')).toBeInTheDocument(); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/tests/components/ProfileSettings.test.tsx` around lines 100 - 123,
The test is asserting for testId "profile-settings-reward-bonus-empty" but
ProfileSettings.tsx always renders testId "profile-settings-reward-bonus" (see
ProfileSettings component and its use of RewardBalanceSparklineCard); either
update the test to expect "profile-settings-reward-bonus" or change
ProfileSettings.tsx to conditionally render the empty variant (e.g., append
"-empty" to the testId when walletStatusState.capabilities.isConnected is false
or when RewardBalanceSparklineCard would render an empty state) so the testId in
the rendered output matches the test assertion.
Description
This PR implements the two frontend backlog items together by wiring shared compact metrics and reward trend surfaces into live overview/profile entry points, with explicit fallback states and targeted test coverage.
Closes #827
Closes #829
Changes proposed
What were you told to do?
Implement issue #827 (shared expandable stat list for compact dashboard modules) and issue #829 (mini reward trend cards for overview/profile surfaces), then ship both in one PR.
What did I do?
Shared compact expandable stat list (#827)
CollapsibleStatsGroupintoGameLobbyas a compact dashboard module (lobby-compact-stats) with summary, trend-aware values, and fallback-safe fields.CollapsibleStatsGroupintoProfileSettingsas an overview panel (profile-settings-overview-stats) for connected state, network/provider context, and draft state.Mini reward trend cards for overview/profile surfaces (#829)
GameLobbyusingRewardBalanceSparklineCardwith real route-reachable entry point and loading fallback.ProfileSettingsusingRewardBalanceSparklineCardwith connected and missing-data behavior.Tests
frontend/tests/components/GameLobby.test.tsxto cover compact stats reachability and reward-trend fallback rendering.frontend/tests/components/ProfileSettings.test.tsxto cover reward trend grid + expandable stats availability and disconnected fallback path.Check List (Check all the applicable boxes)
Screenshots / Testing Evidence
frontend:npx eslint src/pages/GameLobby.tsx src/pages/ProfileSettings.tsx tests/components/GameLobby.test.tsx tests/components/ProfileSettings.test.tsx --ext .ts,.tsxfrontend:npx vitest run tests/components/GameLobby.test.tsx tests/components/ProfileSettings.test.tsxSummary by CodeRabbit
New Features
Tests