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>
140 lines
5.2 KiB
JavaScript
140 lines
5.2 KiB
JavaScript
import Link from 'next/link';
|
|
|
|
/**
|
|
* DashboardFeaturedCollection — the 4x2 card-thumbnail grid panel
|
|
* from the operator's redesign-v2 mockup. Mounted on /dashboard
|
|
* below the stat-card row.
|
|
*
|
|
* Per umbrella convoy § 7.5, the data source is the user's most-
|
|
* recent 8 owned cards (fetched from /api/user-cards by the parent
|
|
* page, passed in here as `cards`). When the user has <8 cards, the
|
|
* empty slots render a clear "Add cards" CTA placeholder. No
|
|
* hardcoded demo cards.
|
|
*
|
|
* Props:
|
|
* cards: array of user_cards JOIN cards rows (or empty array)
|
|
* — each row has at minimum { card_id, name, image_url,
|
|
* set_name, rarity }.
|
|
* loading: boolean — shows shimmer placeholders when true.
|
|
*
|
|
* Implementation notes:
|
|
* - The mockup's "All Sets" filter dropdown and grid/list toggle
|
|
* are intentionally NOT wired here; they're sketches in the
|
|
* mockup and a downstream convoy will own them. Rendering them
|
|
* as decoration with TODO comments would be misleading; they're
|
|
* simply omitted until functional.
|
|
* - Each card uses the .card-grid-outer-glow class from sub-convoy
|
|
* #6 (PR #106) for the warm outer glow treatment.
|
|
*/
|
|
export default function DashboardFeaturedCollection({ cards = [], loading = false }) {
|
|
const slots = Array.from({ length: 8 }, (_, idx) => cards[idx] || null);
|
|
|
|
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="M12 2c-.5 3.5-3.5 5.5-3.5 9 0 2.5 1.5 4 3.5 4s3.5-1.5 3.5-4c0-1.5-1-3-2-4 1 2 .5 4-.5 5-1 1-2-1-1-3 .5-1 1.5-3 0-7z" />
|
|
</svg>
|
|
</span>
|
|
Featured Collection
|
|
</h2>
|
|
<Link
|
|
href="/my-cards"
|
|
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
|
|
</Link>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
{slots.map((card, idx) => {
|
|
if (loading) {
|
|
return (
|
|
<div
|
|
key={`loading-${idx}`}
|
|
className="aspect-[2.5/3.5] rounded-lg motion-essential animate-pulse"
|
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
|
aria-hidden="true"
|
|
/>
|
|
);
|
|
}
|
|
if (!card) {
|
|
return (
|
|
<Link
|
|
key={`empty-${idx}`}
|
|
href="/cards"
|
|
className="aspect-[2.5/3.5] rounded-lg flex flex-col items-center justify-center text-center p-2 transition-colors focus:outline-none focus:ring-2"
|
|
style={{
|
|
border: '2px dashed var(--border)',
|
|
color: 'var(--text-secondary)',
|
|
'--tw-ring-color': 'var(--accent-ember)',
|
|
}}
|
|
>
|
|
<svg
|
|
className="h-6 w-6 mb-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
|
/>
|
|
</svg>
|
|
<span className="text-xs font-medium">Add Card</span>
|
|
</Link>
|
|
);
|
|
}
|
|
return (
|
|
<Link
|
|
key={card.id ?? card.card_id ?? idx}
|
|
href={`/card/${card.card_id ?? card.id}`}
|
|
className="card-grid-outer-glow aspect-[2.5/3.5] rounded-lg overflow-hidden block transition-transform hover:scale-105 focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
'--tw-ring-color': 'var(--accent-ember)',
|
|
}}
|
|
aria-label={card.name ?? 'Card'}
|
|
>
|
|
{card.image_url ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={card.image_url}
|
|
alt={card.name ?? ''}
|
|
className="w-full h-full object-cover"
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<div
|
|
className="w-full h-full flex items-center justify-center text-center p-2 text-xs"
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
>
|
|
{card.name ?? 'Card'}
|
|
</div>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|