+
+ );
+}
diff --git a/components/DashboardRecentActivity.js b/components/DashboardRecentActivity.js
new file mode 100644
index 0000000..634268f
--- /dev/null
+++ b/components/DashboardRecentActivity.js
@@ -0,0 +1,155 @@
+/**
+ * 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 (
+
+
+
+
+
+
+ Recent Activity
+
+
+
+
+ {usingDemo && (
+
+ {/* 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.
+
+ )}
+
+
+ {rows.map((row) => (
+
+
+ {row.actor.initial}
+
+
+
+ {row.actor.name}{' '}
+
+ {row.action}
+
+
+
+ {row.subject}
+
+
+
+ {row.time}
+
+
+ ))}
+
+
+ );
+}
diff --git a/pages/dashboard.js b/pages/dashboard.js
index 6b2b3ab..345e866 100644
--- a/pages/dashboard.js
+++ b/pages/dashboard.js
@@ -1,18 +1,36 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
+import Link from 'next/link';
import Layout from '../components/Layout';
-import PermissionIndicator from '../components/PermissionIndicator';
+import DashboardFeaturedCollection from '../components/DashboardFeaturedCollection';
+import DashboardRecentActivity from '../components/DashboardRecentActivity';
+import DashboardCardSpotlight from '../components/DashboardCardSpotlight';
import { Button, StatCard } from '../components/ui';
import { useAuth } from '../lib/use-auth';
-import Link from 'next/link';
-import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
+// Dashboard rebuild — redesign-v2 sub-convoys #7 + #8 (2026-06-04).
+// Layout per operator mockup:
+// row 1: 4-up StatCard grid (Total Cards / Rare Cards / Collection
+// Value / Wishlist Items) — locked by § 7.1 of umbrella convoy.
+// row 2: 2-col layout (lg:grid-cols-3) — left col (2/3) holds
+// Featured Collection grid + Recent Activity feed;
+// right col (1/3) holds the Card Spotlight rail.
+// Mobile: stacks vertically.
+//
+// Data:
+// - Collections (real) drives Total Cards + Collection Value.
+// - Most-recent 8 user-owned cards (real) drives Featured Collection.
+// - Rare Cards / Wishlist / Recent Activity / Card Spotlight all
+// ship with operator-approved placeholders + TODO comments to
+// the follow-up convoys that will land real data.
export default function Dashboard() {
const router = useRouter();
const { user, loading: authLoading } = useAuth();
-
+
const [collections, setCollections] = useState([]);
+ const [recentCards, setRecentCards] = useState([]);
const [loading, setLoading] = useState(true);
+ const [cardsLoading, setCardsLoading] = useState(true);
// Redirect to login if not authenticated
useEffect(() => {
@@ -21,113 +39,117 @@ export default function Dashboard() {
}
}, [authLoading, user, router]);
- const fetchCollections = async () => {
- try {
- const token = localStorage.getItem('auth_token');
- const headers = {
- 'Content-Type': 'application/json',
- };
-
- if (token) {
- headers.Authorization = `Bearer ${token}`;
- }
-
- const response = await fetch('/api/collections', { headers });
-
- if (response.ok) {
- const data = await response.json();
-
- // Fetch thumbnails for each collection
- const collectionsWithThumbnails = await Promise.all(
- data.map(async (collection) => {
- try {
- const identifier = collection.slug || collection.id;
- const thumbnailResponse = await fetch(`/api/collections/${identifier}/thumbnails`, { headers });
- if (thumbnailResponse.ok) {
- const thumbnailData = await thumbnailResponse.json();
- return { ...collection, thumbnails: thumbnailData.thumbnails };
- }
- return { ...collection, thumbnails: [] };
- } catch (error) {
- console.error(`Error fetching thumbnails for collection ${collection.id}:`, error);
- return { ...collection, thumbnails: [] };
- }
- })
- );
-
- setCollections(collectionsWithThumbnails);
- } else {
- console.error('Failed to fetch collections');
- setCollections([]);
- }
- } catch (error) {
- console.error('Error fetching collections:', error);
- setCollections([]);
- } finally {
- setLoading(false);
- }
- }
-
useEffect(() => {
- if (user) {
- // eslint-disable-next-line react-hooks/set-state-in-effect -- load dashboard lists when user is available
- fetchCollections();
- }
+ if (!user) return;
+ let cancelled = false;
+
+ const fetchAll = async () => {
+ const token = localStorage.getItem('auth_token');
+ const headers = { 'Content-Type': 'application/json' };
+ if (token) headers.Authorization = `Bearer ${token}`;
+
+ try {
+ const [collectionsRes, cardsRes] = await Promise.all([
+ fetch('/api/collections', { headers }),
+ fetch('/api/user-cards', { headers }),
+ ]);
+
+ if (!cancelled) {
+ if (collectionsRes.ok) {
+ const data = await collectionsRes.json();
+ setCollections(Array.isArray(data) ? data : []);
+ } else {
+ setCollections([]);
+ }
+ if (cardsRes.ok) {
+ const data = await cardsRes.json();
+ // /api/user-cards returns rows ordered by created_at DESC;
+ // take the 8 most-recent for the Featured Collection grid.
+ setRecentCards(Array.isArray(data) ? data.slice(0, 8) : []);
+ } else {
+ setRecentCards([]);
+ }
+ }
+ } catch (error) {
+ console.error('[dashboard] fetch error:', error);
+ if (!cancelled) {
+ setCollections([]);
+ setRecentCards([]);
+ }
+ } finally {
+ if (!cancelled) {
+ setLoading(false);
+ setCardsLoading(false);
+ }
+ }
+ };
+
+ fetchAll();
+ return () => {
+ cancelled = true;
+ };
}, [user]);
-;
+ const totalCards = collections.reduce(
+ (total, col) => total + (col.cardCount || 0),
+ 0
+ );
+ const collectionValue = collections.reduce(
+ (total, col) => total + (col.value || 0),
+ 0
+ );
return (
- {/* Header */}
-
+
+ {/* Page heading lives inside the content area, not in a
+ heavy header strip. The top chrome (search, notifs,
+ avatar) is provided by in Layout. */}
-
- {VOCAB.MY_COLLECTION}
+
+ Welcome back
+ {user?.username ? `, ${user.username}` : ''}
-
- Overview of your lists and owned cards
+
+ Here's what's happening with your collection today.
-
+
+
+
+
-
-
- {/* Content */}
-
{loading ? (
-
+
) : (
-
- {/* Stats Cards — 4-up grid from operator mockup
- (.convoys/redesign-v2-from-mockups.md § 7.1).
- Metrics: Total Cards / Rare Cards / Collection Value /
- Wishlist Items. Real data where available; placeholders
- with TODO comments where the concept doesn't exist yet. */}
-