8d650fefae
CI / build-and-push (push) Successful in 28s
Address 18 issues across high/medium/low impact tiers identified in a full interface review. Key changes: Models page decomposed into tabs, confirmation dialogs for irreversible actions (deploy/open-source/acquire), chart Y-axes made visible, hash router extended for Market tab persistence, collapsible sidebar, keyboard navigation shortcuts (g+key chords), notification bulk actions, achievement progress bars, and ARIA label improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
91 lines
3.8 KiB
TypeScript
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 '@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 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>
|
|
);
|
|
}
|