deckhearth/components/DashboardRecentActivity.js

156 lines
5.1 KiB
JavaScript
Raw Permalink Normal View History

feat(design-system): redesign v2 #7 + #8 — dashboard rebuild + right-rail Card Spotlight (#107) Final integration PR for the redesign-v2 epic. Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both restructure pages/dashboard.js. Sub-convoy #7 — dashboard layout rebuild Three new dashboard-only components: - components/DashboardFeaturedCollection.js: 4x2 grid of the user's most-recent 8 owned cards (real data from /api/user-cards per umbrella § 7.5). Empty slots render a "+ Add Card" CTA linking to /cards. Each card surface uses .card-grid-outer-glow from sub- convoy #6 (PR #106) for the warm outer-glow treatment. The mockup's "All Sets" filter dropdown + grid/list toggle are intentionally omitted (decoration without functionality would be misleading — a downstream convoy will wire them). - components/DashboardRecentActivity.js: avatar + text + timestamp rows pattern. A user-wide activity feed API does not exist yet (collection_activity is per-collection); ships with 3 demo rows and a TODO comment + small "Demo activity" banner pointing at the follow-up convoy that will land /api/user/activity. - pages/dashboard.js: full rewrite of the page body. Heading lives inside the content area now (Layout's TopSearchBar from sub- convoy #3 provides the top chrome). Stats row stays (4-up). Below stats: lg:grid-cols-3 with featured-collection + activity in the left 2/3 and the new Card Spotlight rail in the right 1/3. Mobile stacks vertically. Data fetch consolidated into a single useEffect that hits /api/collections + /api/user-cards in parallel, with cancellation guard. Sub-convoy #8 — right-rail Card Spotlight (sketch tier) - components/DashboardCardSpotlight.js: glass-panel rail with card preview + metadata table (Rarity / Set / Collector # / Condition) + Market Value $128.47 + delta +18.6% (30d) + Price Trend line chart (inline SVG, 30 daily samples) + Market Overview area chart (inline SVG with linearGradient fill) + Watchlist of 3 mini card rows with value + delta. - Per umbrella § 8: this is the sketch tier. Real market-value API, real watchlist storage, real price-history are out of scope. TODO comment + "Demo data" banner mark the placeholder boundary. - Per umbrella § 2 "No new dependency": charts are inline SVG, no charting library added. Path data is hand-shaped (~30 samples) to match the mockup's gentle climb-then-peak shape. Accessibility: - Charts carry role="img" + aria-label describing the metric and trend direction (e.g. "Market overview area chart, 7 day change positive"). - Card preview carries role="img" with the card name. - Watchlist rows carry aria-label tying card name + value + delta. Tests: - npm run test:run: 113/113 - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green This completes the redesign-v2 epic (8/8 sub-convoys merged once this lands). Updated .convoys/redesign-v2-from-mockups.md frontmatter status to "shipped" after merge. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:21:58 -04:00
/**
* DashboardRecentActivity the avatar + text + timestamp list panel
* from the operator's redesign-v2 mockup.
*
* Implementation note: a user-wide activity feed API does not exist
* yet (collection_activity is scoped per-collection). Per the
* operator's pattern for "real metrics where available, placeholders
* for what we don't have" (umbrella § 7.1), this component ships
* with hardcoded demo rows and a TODO comment pointing at the
* follow-up convoy that will land /api/user/activity.
*
* Once the API ships, the parent page will fetch and pass rows in
* via the `activities` prop; the component's render code already
* handles both real and demo shapes (same { id, actor, action,
* subject, time } shape).
*
* Props:
* activities: array | undefined if undefined or empty, demo rows
* render. If a non-empty array is passed, those rows
* render instead (forward-compat).
*/
const DEMO_ACTIVITIES = [
{
id: 'demo-1',
actor: { name: 'StarGazer73', initial: 'S' },
action: 'completed a trade',
subject: '2x Astral Sage for Tideborn Explorer',
time: '2m ago',
},
{
id: 'demo-2',
actor: { name: 'You', initial: 'Y' },
action: 'listed a card for sale',
subject: 'Voidforge Titan • $89.99',
time: '18m ago',
},
{
id: 'demo-3',
actor: { name: 'Market', initial: 'M' },
action: 'price drop alert',
subject: 'Lumen Warden is down 12%',
time: '1h ago',
},
];
const ACTOR_GRADIENTS = {
S: 'linear-gradient(135deg, rgb(120, 144, 255) 0%, rgb(80, 96, 207) 100%)',
Y: 'linear-gradient(135deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
M: 'linear-gradient(135deg, rgb(178, 102, 255) 0%, rgb(124, 58, 237) 100%)',
};
function gradientFor(initial) {
return (
ACTOR_GRADIENTS[initial] ??
'linear-gradient(135deg, rgb(150, 150, 150) 0%, rgb(100, 100, 100) 100%)'
);
}
export default function DashboardRecentActivity({ activities }) {
const rows = activities && activities.length > 0 ? activities : DEMO_ACTIVITIES;
const usingDemo = !activities || activities.length === 0;
return (
<section className="glass-panel rounded-2xl p-5 sm:p-6">
<header className="flex items-center justify-between mb-4">
<h2
className="text-lg font-bold flex items-center gap-2"
style={{ color: 'var(--text-primary)' }}
>
<span
className="inline-flex h-5 w-5 items-center justify-center rounded-md"
style={{
background:
'linear-gradient(135deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
}}
aria-hidden="true"
>
<svg
className="h-3.5 w-3.5"
viewBox="0 0 24 24"
fill="rgb(255, 255, 255)"
>
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</span>
Recent Activity
</h2>
<button
type="button"
className="text-sm font-medium hover:underline focus:outline-none focus:ring-2 rounded-md px-1"
style={{
color: 'var(--accent-ember)',
'--tw-ring-color': 'var(--accent-ember)',
}}
>
View All
</button>
</header>
{usingDemo && (
<p
className="text-xs mb-3 italic"
style={{ color: 'var(--text-secondary)' }}
>
{/* TODO(user-activity-feed convoy): replace these demo rows
with rows from GET /api/user/activity once that endpoint
ships. Operator-approved placeholder per umbrella § 7.1. */}
Demo activity connect to live feed in a follow-up release.
</p>
)}
<ul className="space-y-3">
{rows.map((row) => (
<li key={row.id} className="flex items-start gap-3">
<div
className="w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0"
style={{
background: gradientFor(row.actor.initial),
color: 'rgb(255, 255, 255)',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.18)',
}}
aria-hidden="true"
>
{row.actor.initial}
</div>
<div className="flex-1 min-w-0">
<div
className="text-sm"
style={{ color: 'var(--text-primary)' }}
>
<span className="font-semibold">{row.actor.name}</span>{' '}
<span style={{ color: 'var(--text-secondary)' }}>
{row.action}
</span>
</div>
<div
className="text-xs truncate"
style={{ color: 'var(--text-secondary)' }}
>
{row.subject}
</div>
</div>
<span
className="text-xs whitespace-nowrap flex-shrink-0"
style={{ color: 'var(--text-secondary)' }}
>
{row.time}
</span>
</li>
))}
</ul>
</section>
);
}