Skip to content

feat: add shared compact stats and reward trend cards#858

Merged
CMI-James merged 1 commit into
theblockcade:mainfrom
FrostGraphix:codex/close-827-829
May 30, 2026
Merged

feat: add shared compact stats and reward trend cards#858
CMI-James merged 1 commit into
theblockcade:mainfrom
FrostGraphix:codex/close-827-829

Conversation

@FrostGraphix
Copy link
Copy Markdown
Contributor

@FrostGraphix FrostGraphix commented May 30, 2026

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)

  • Integrated CollapsibleStatsGroup into GameLobby as a compact dashboard module (lobby-compact-stats) with summary, trend-aware values, and fallback-safe fields.
  • Integrated CollapsibleStatsGroup into ProfileSettings as an overview panel (profile-settings-overview-stats) for connected state, network/provider context, and draft state.
  • Kept props narrow and page wiring declarative through memoized stat item arrays.

Mini reward trend cards for overview/profile surfaces (#829)

  • Added mini reward trend card surface to GameLobby using RewardBalanceSparklineCard with real route-reachable entry point and loading fallback.
  • Added mini reward trend card surface to ProfileSettings using RewardBalanceSparklineCard with connected and missing-data behavior.
  • Ensured responsive layout and non-overlapping compact placement for cards in dashboard/profile sections.

Tests

  • Extended frontend/tests/components/GameLobby.test.tsx to cover compact stats reachability and reward-trend fallback rendering.
  • Extended frontend/tests/components/ProfileSettings.test.tsx to cover reward trend grid + expandable stats availability and disconnected fallback path.

Check List (Check all the applicable boxes)

  • My code follows the code style of this project.
  • This PR does not contain plagiarized content.
  • The title and description of the PR is clear and explains the approach.
  • I am making a pull request against the main branch (left side).
  • My commit messages styles matches our requested structure.
  • My code additions will fail neither code linting checks nor unit test.
  • I am only making changes to files I was requested to.

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,.tsx
  • frontend: npx vitest run tests/components/GameLobby.test.tsx tests/components/ProfileSettings.test.tsx

Summary by CodeRabbit

  • New Features

    • Added reward trend visualization with sparkline cards to the game lobby
    • Introduced collapsible compact statistics displaying key wallet and game metrics
    • Added reward balance tracking and trend visualization to profile settings
    • Enhanced responsive grid layout for mobile devices
  • Tests

    • Added test coverage for lobby statistics and reward trends
    • Added test coverage for profile reward display functionality

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Reward Trend and Compact Stats Integration

Layer / File(s) Summary
GameLobby Reward Trend Cards and Compact Stats
frontend/src/pages/GameLobby.tsx, frontend/src/pages/GameLobbyDashboard.css, frontend/tests/components/GameLobby.test.tsx
GameLobby imports reward sparkline and collapsible stats components, builds memoized compactLobbyStats (wallet readiness, pending actions, active games delta, prize signal) and rewardTrendCards ("Prize Reserve" and "Arena Participation" from prize signal, active games, queue health), renders a responsive reward trend grid with loading fallback, collapsible compact stats group, and includes test coverage for empty games + collapsible toggle.
ProfileSettings Reward Trend and Overview Stats
frontend/src/pages/ProfileSettings.tsx, frontend/tests/components/ProfileSettings.test.tsx
ProfileSettings imports reward and collapsible components, derives profileSummaryStats (wallet-connected, network, provider, draft state) and reward series from username length and wallet status, inserts reward trend grid ("earned rewards", "bonus momentum") and collapsible stats group into "Wallet" section. Test setup refactors shared wallet mock state and adds assertions for reward grid, overview stats, and disconnected wallet empty fallback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • theblockcade/stellarcade#760: This PR's integration of CollapsibleStatsGroup and RewardBalanceSparklineCard components in GameLobby and ProfileSettings directly depends on the component implementations introduced in PR #760.

Poem

🐰 A sparkline shimmers in the lobby glow,
Rewards and stats are watching the flow,
With collapsible grace and trends that align,
Dashboard metrics now simply shine!
Stats expand like carrots, expandable and bright—
Frontend dashboards now just right! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding shared compact stats and reward trend cards across GameLobby and ProfileSettings components.
Linked Issues check ✅ Passed The PR implements both issues #827 and #829: CollapsibleStatsGroup (compact expandable stats) and RewardBalanceSparklineCard (mini reward trends) are integrated into GameLobby and ProfileSettings with explicit state handling, responsive CSS, and targeted test coverage.
Out of Scope Changes check ✅ Passed All changes are in-scope: component integration, CSS layout for new UI sections, memoized data builders, and test coverage directly support the linked issues with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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: 2

🧹 Nitpick comments (1)
frontend/src/pages/ProfileSettings.tsx (1)

314-322: ⚡ Quick win

Prefer CSS class over inline styles for consistency.

The reward trend grid uses inline styles, while GameLobby.tsx uses the .lobby-reward-trend-grid CSS 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4ecc761 and dec9833.

📒 Files selected for processing (5)
  • frontend/src/pages/GameLobby.tsx
  • frontend/src/pages/GameLobbyDashboard.css
  • frontend/src/pages/ProfileSettings.tsx
  • frontend/tests/components/GameLobby.test.tsx
  • frontend/tests/components/ProfileSettings.test.tsx

Comment on lines +687 to +690
activeGamesDelta === null
? "0%"
: `${activeGamesDelta > 0 ? "+" : ""}${Math.min(Math.abs(activeGamesDelta) * 5, 25)}%`,
trend: activeGamesDelta !== null && activeGamesDelta < 0 ? ("down" as const) : ("up" as const),
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 | ⚡ Quick win

🧩 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" --heading

Repository: 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).

Comment on lines +100 to 123
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();
});
});
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 | 🔴 Critical | ⚡ Quick win

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.

Suggested change
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.

@CMI-James CMI-James merged commit 1b85178 into theblockcade:main May 30, 2026
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants