Files
AIHostingTycoon/apps/web/src/components/common/NotificationPanel.tsx
T
josh c1cc70eeb9
Balance Check / balance-simulation (pull_request) Successful in 38s
Balance Check / multi-run-balance (pull_request) Successful in 13m44s
Rename AI Tycoon to Token Empire across entire codebase
Full rebrand: UI display text, package scope (@ai-tycoon/* -> @token-empire/*),
localStorage keys, Docker/CI image paths, database names, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 21:04:07 -04:00

91 lines
3.8 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { X, CheckCircle, AlertTriangle, AlertCircle, Info, Bell, Trash2 } from 'lucide-react';
import { useGameStore, type GameNotification } from '@/store';
import { formatDuration } from '@token-empire/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 removeNotification = useGameStore((s) => s.removeNotification);
const clearAll = useGameStore((s) => s.clearAllNotifications);
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>
<div className="flex items-center gap-2">
{notifications.length > 0 && (
<button onClick={clearAll} className="text-surface-500 hover:text-surface-300 text-[10px]" title="Clear all">
<Trash2 size={12} />
</button>
)}
<button onClick={onClose} className="text-surface-400 hover:text-surface-200">
<X size={14} />
</button>
</div>
</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 group">
<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>
<button onClick={() => removeNotification(n.id)} className="text-surface-600 hover:text-surface-300 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 mt-0.5">
<X size={12} />
</button>
</div>
</div>
);
})
)}
</div>
</div>
);
}