feat(dashboard): animate value figures with slot-text, drop dead odometer#39
Conversation
…eter Adopt the slot-text library (pinned 0.3.2, dependency-free, ~2KB) to roll the dashboard headline inventory value and the selected-day holdings total whenever they change (sync, day stepping, chart clicks). The new components/ui/animated-text.tsx wrapper is the single tuning point and adds a prefers-reduced-motion fallback the library lacks. Delete the hand-rolled animated-number.tsx odometer and completion-ring.tsx: both were dead code inherited from the base project (the ring was never rendered, and it was the odometer's only consumer). Closes #38 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds ChangesAnimated Text Adoption
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@package.json`:
- Line 55: The dependency version for slot-text is pointing at a non-existent
release, which will break installation in CI. Update the slot-text entry in
package.json to a published npm version that actually exists, and keep the
dependency name unchanged so pnpm can resolve it successfully.
🪄 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 Plus
Run ID: 78a59a31-da79-4c73-b84c-141ce1749962
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
CLAUDE.mdcomponents/dashboard/inventory-value-panel.tsxcomponents/ui/animated-number.tsxcomponents/ui/animated-text.tsxcomponents/ui/completion-ring.tsxpackage.jsontest/animated-text.test.tsx
💤 Files with no reviewable changes (2)
- components/ui/animated-number.tsx
- components/ui/completion-ring.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
components/ui/**
📄 CodeRabbit inference engine (CLAUDE.md)
Use the shared shadcn/ui primitive components and related UI utilities from the
components/ui/directory for reusable interface elements.
Files:
components/ui/animated-text.tsx
components/dashboard/inventory-value-panel.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Show the latest inventory value and a Recharts history line, with a manual “Sync now” button.
Files:
components/dashboard/inventory-value-panel.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: finallyjay/csgo-inventory-tracker
Timestamp: 2026-07-08T11:50:14.016Z
Learning: Use the provided pnpm commands for development, build, linting, type-checking, testing, formatting, and running single Vitest files.
Learnt from: CR
Repo: finallyjay/csgo-inventory-tracker
Timestamp: 2026-07-08T11:50:14.016Z
Learning: CI runs install → lint → test → build, and pre-commit hooks run oxfmt + oxlint via Husky and lint-staged.
Learnt from: CR
Repo: finallyjay/csgo-inventory-tracker
Timestamp: 2026-07-08T11:50:14.016Z
Learning: The project is a Next.js 16 App Router application using React 19, TypeScript strict mode, Tailwind CSS 4, and shadcn/ui + Radix UI primitives.
🔇 Additional comments (5)
components/ui/animated-text.tsx (2)
17-25: SSR-safe reduced-motion detection.
useSyncExternalStorewith agetServerSnapshotof() => falsecorrectly avoids callingwindow.matchMediaduring SSR/hydration. No issues here.
37-45: LGTM!components/dashboard/inventory-value-panel.tsx (1)
6-6: LGTM!Also applies to: 129-131, 245-247
test/animated-text.test.tsx (1)
1-47: LGTM!CLAUDE.md (1)
160-167: LGTM!
…feel The inventory list swapped the whole grid for skeletons on every reload, so nothing could animate after a sync — refetch in the background instead, keeping the cards mounted while their prices roll (list total, line totals and per-unit prices now use AnimatedText). On the dashboard, every post-sync refetch handed recharts a fresh data array and the line replayed its full draw animation, which read as the entire panel reloading. Disable the line animation; the rolling value is the sync feedback now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/inventory/inventory-list.tsx (1)
120-139: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBackground refresh failure still wipes the mounted list.
backgroundonly gatessetLoading(true). On the!res.okbranch and incatch,setData(null)/setError(...)still run unconditionally, flipping the render to the error branch (Line 245) and unmounting the list — exactly the skeleton-swap/mount-churn this change is meant to avoid on a sync-triggered background refresh.🐛 Proposed fix to keep the list mounted on background failures
const load = useCallback(async (background = false) => { if (!background) setLoading(true) - setError(null) + if (!background) setError(null) try { const res = await fetch("/api/inventory/items", { cache: "no-store" }) const body = await res.json() if (!res.ok) { - setError(body.error ?? "Failed to load inventory.") - setData(null) + if (background) { + toast({ title: "Refresh failed", description: body.error ?? "Unknown error", variant: "destructive" }) + } else { + setError(body.error ?? "Failed to load inventory.") + setData(null) + } return } setData(body as InventoryItemsResponse) } catch { - setError("Network error while loading inventory.") + if (background) { + toast({ title: "Network error", variant: "destructive" }) + } else { + setError("Network error while loading inventory.") + } } finally { setLoading(false) } }, [])Also applies to: 155-155
🤖 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 `@components/inventory/inventory-list.tsx` around lines 120 - 139, The `load` callback in `inventory-list.tsx` still clears `data` and sets an error on failed requests even when `background` is true, which causes the mounted list to unrender during background refreshes. Update `load` so the `!res.ok` and `catch` paths preserve the current `data` when `background` is true, and only call `setError`/`setData(null)` for foreground loads; keep `setLoading(false)` in `finally` as-is. Use the `load` callback and the background refresh flow to locate the change.
🤖 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.
Outside diff comments:
In `@components/inventory/inventory-list.tsx`:
- Around line 120-139: The `load` callback in `inventory-list.tsx` still clears
`data` and sets an error on failed requests even when `background` is true,
which causes the mounted list to unrender during background refreshes. Update
`load` so the `!res.ok` and `catch` paths preserve the current `data` when
`background` is true, and only call `setError`/`setData(null)` for foreground
loads; keep `setLoading(false)` in `finally` as-is. Use the `load` callback and
the background refresh flow to locate the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 414eb2fb-7a41-4750-acff-82a9111b7070
📒 Files selected for processing (2)
components/dashboard/inventory-value-panel.tsxcomponents/inventory/inventory-list.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Verify the session cookie signature, re-check whitelist access on every auth lookup, and derive
isAdminfromADMIN_STEAM_IDrather than storing it in the cookie.
Files:
components/dashboard/inventory-value-panel.tsxcomponents/inventory/inventory-list.tsx
**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Use the design tokens defined by the project theme instead of hardcoded white-based utility colors when styling React components.
Files:
components/dashboard/inventory-value-panel.tsxcomponents/inventory/inventory-list.tsx
**/components/dashboard/inventory-value-panel.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Include the latest inventory value, the history line chart, and the manual "Sync now" action in the dashboard value panel.
Files:
components/dashboard/inventory-value-panel.tsx
🔇 Additional comments (3)
components/dashboard/inventory-value-panel.tsx (2)
6-6: LGTM!Also applies to: 129-131, 249-251
187-190: LGTM!components/inventory/inventory-list.tsx (1)
6-6: LGTM!Also applies to: 80-86, 179-181
AnimatedText now masks every digit to 0 on first paint and rolls the real digits in right after mount, so the animation plays on every page load and remount — not only when a value changes later. Width stays stable (same character count) and the accessible name is pinned to the real value via aria-label while the masked frame is up. Extend the roll to the remaining price figures: holdings rows (line total and per-unit, also rolling in place when stepping days thanks to their stable keys) and the Latest/Low/High stats on the item detail page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed post-sync refetch went through the same setData(null)/setError path as the initial load, wiping the list the background mode was meant to keep on screen. Toast the failure instead and leave the data as-is; the initial-load error UI is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
components/inventory/holding-row.tsx (1)
43-49: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider mount-triggered animation cost at scale.
Since
AnimatedTextmasks digits on mount and rolls in viauseEffecton every mount, everyHoldingRowin a large inventory list will animate simultaneously whenever the list (re)mounts (initial load, filter/sort changes causing key changes, etc.), not just on real price updates. Worth verifying this doesn't produce a jarring "cascade" effect or performance hit for large inventories.🤖 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 `@components/inventory/holding-row.tsx` around lines 43 - 49, The issue is that `AnimatedText` in `HoldingRow` will re-run its mount animation for every row whenever the list remounts or keys change, which can cause a cascade effect and extra work in large inventories. Update `HoldingRow` and/or `AnimatedText` so the animation is only triggered when the displayed price value actually changes, not on every mount; use the existing `AnimatedText` usage around `formatPrice(item.lineTotal ?? 0, currency)` and `formatPrice(item.unitPrice, currency)` to locate the affected render paths.
🤖 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.
Nitpick comments:
In `@components/inventory/holding-row.tsx`:
- Around line 43-49: The issue is that `AnimatedText` in `HoldingRow` will
re-run its mount animation for every row whenever the list remounts or keys
change, which can cause a cascade effect and extra work in large inventories.
Update `HoldingRow` and/or `AnimatedText` so the animation is only triggered
when the displayed price value actually changes, not on every mount; use the
existing `AnimatedText` usage around `formatPrice(item.lineTotal ?? 0,
currency)` and `formatPrice(item.unitPrice, currency)` to locate the affected
render paths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e180f9f8-59fe-4f69-b8fe-8e8769a27bae
📒 Files selected for processing (4)
components/inventory/holding-row.tsxcomponents/inventory/inventory-list.tsxcomponents/inventory/item-price-chart.tsxcomponents/ui/animated-text.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- components/ui/animated-text.tsx
- components/inventory/inventory-list.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use the project’s UTC date convention: persist timestamps and snapshot days in UTC, treatformatDay()as a UTC bucket key that must never timezone-shift, and useformatDateTime()only for real timestamps displayed in the viewer’s timezone.
Store and compute money as integer minor units everywhere (never floats) to avoid precision drift.
Use the repository’s authenticated-session conventions: verify the signedsteam_usercookie, re-check whitelist access on each auth-gated request, and derive admin status fromADMIN_STEAM_IDrather than storing it in the cookie.
Prefer client-safe, pure helpers for shared utility modules when possible, and keep server-only behavior isolated to server modules.
Use@/*as the project-root path alias for imports.
Files:
components/inventory/holding-row.tsxcomponents/inventory/item-price-chart.tsx
🔇 Additional comments (2)
components/inventory/holding-row.tsx (1)
4-4: LGTM!Also applies to: 43-49
components/inventory/item-price-chart.tsx (1)
6-6: LGTM!Also applies to: 38-40
Closes #38
What
slot-text(pinned0.3.2, dependency-free, ~2KB gzip, MIT) and a thincomponents/ui/animated-text.tsxwrapper — the single tuning point for roll options, plus aprefers-reduced-motionfallback (plain<span>) that the library doesn't provide.formatPrice()output — symbol, separators and decimals included.animated-number.tsxodometer andcompletion-ring.tsx: both dead code from the base project (the ring was never rendered anywhere, and it was the odometer's only importer). Updates the UI-kit list in CLAUDE.md.Why slot-text over the hand-rolled odometer
The odometer only handled ungrouped integers (no symbol/decimals), always rolled from 0 on mount, and was 113 lines of unused maintenance surface. slot-text animates any short string per-character; its one caveat (kerning/ligature loss) is moot with the theme's monospace fonts.
Tests
test/animated-text.test.tsx(jsdom): renders through slot-text with the text as accessible name (aria-label), falls back to a plain span under reduced motion, and updates the accessible name on prop change.pnpm lint, full suite (85 tests) andpnpm buildall green.Note: not verified visually in a browser — the dashboard is behind Steam auth; verify with
pnpm dev→ sync / step days.🤖 Generated with Claude Code