Files
AIHostingTycoon/apps/web/src/components/common/NotificationPanel.tsx
T
josh f9f6233b69
CI / build-and-push (push) Successful in 33s
Comprehensive UX polish: fix 19 friction points across all pages
Addresses broken interactions (notification bell, browser dialogs),
missing feedback states (disabled buttons, pricing changes, paused
indicator), unclear affordances (research queue, model tuning, funding
requirements), and navigation gaps (hash routing, keyboard shortcuts,
clickable dashboard cards, sidebar grouping, tutorial hints).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 21:44:18 -04:00

79 lines
3.1 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { X, CheckCircle, AlertTriangle, AlertCircle, Info, Bell } from 'lucide-react';
import { useGameStore, type GameNotification } from '@/store';
import { formatDuration } from '@ai-tycoon/shared';
const ICON_MAP = {
success: { icon: CheckCircle, color: 'text-success' },
warning: { icon: AlertTriangle, color: 'text-warning' },
danger: { icon: AlertCircle, color: 'text-danger' },
info: { icon: Info, color: 'text-accent-light' },
} as const;
export function NotificationPanel({ onClose }: { onClose: () => void }) {
const notifications = useGameStore((s) => s.notifications);
const markAllRead = useGameStore((s) => s.markAllNotificationsRead);
const currentTick = useGameStore((s) => s.meta.tickCount);
const panelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
markAllRead();
}, [markAllRead]);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
onClose();
}
};
const keyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('mousedown', handler);
document.addEventListener('keydown', keyHandler);
return () => {
document.removeEventListener('mousedown', handler);
document.removeEventListener('keydown', keyHandler);
};
}, [onClose]);
return (
<div
ref={panelRef}
className="absolute top-12 right-0 w-80 max-h-96 bg-surface-900 border border-surface-700 rounded-xl shadow-2xl z-50 overflow-hidden flex flex-col"
>
<div className="px-4 py-3 border-b border-surface-700 flex items-center justify-between shrink-0">
<h3 className="text-sm font-semibold">Notifications</h3>
<button onClick={onClose} className="text-surface-400 hover:text-surface-200">
<X size={14} />
</button>
</div>
<div className="overflow-y-auto flex-1">
{notifications.length === 0 ? (
<div className="p-6 text-center text-surface-500">
<Bell size={24} className="mx-auto mb-2 opacity-50" />
<p className="text-sm">No notifications yet</p>
</div>
) : (
notifications.map((n: GameNotification) => {
const { icon: Icon, color } = ICON_MAP[n.type] ?? ICON_MAP.info;
const ticksAgo = currentTick - n.tick;
return (
<div key={n.id} className="px-4 py-3 border-b border-surface-800 last:border-0 hover:bg-surface-800/50">
<div className="flex items-start gap-2">
<Icon size={14} className={`${color} mt-0.5 shrink-0`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium">{n.title}</div>
<div className="text-xs text-surface-400">{n.message}</div>
<div className="text-xs text-surface-600 mt-1">{formatDuration(ticksAgo)} ago</div>
</div>
</div>
</div>
);
})
)}
</div>
</div>
);
}