156 lines
5.1 KiB
JavaScript
156 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>
|
||
|
|
);
|
||
|
|
}
|