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>
155 lines
5.1 KiB
JavaScript
155 lines
5.1 KiB
JavaScript
/**
|
|
* 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>
|
|
);
|
|
}
|