Add 10 major features to complete the desktop wallet: - Staking: Stake SYN tokens for rewards with pool management - DEX/Swap: Built-in token swap interface with liquidity pools - Address Book: Save and manage frequently used addresses - DApp Browser: Interact with decentralized applications - Hardware Wallet: Ledger/Trezor support for secure signing - Multi-sig Wallets: Require multiple signatures for transactions - Price Charts: Market data and real-time price tracking - Notifications: Push notifications for transactions and alerts - QR Scanner: Generate and parse payment QR codes - Backup/Export: Encrypted wallet backup and recovery Includes Tauri backend commands for all features, Zustand stores for state management, and complete UI pages with navigation.
277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Bell,
|
|
BellOff,
|
|
Settings,
|
|
X,
|
|
Send,
|
|
Hammer,
|
|
Coins,
|
|
AlertCircle,
|
|
Info,
|
|
} from 'lucide-react';
|
|
import {
|
|
useNotificationsStore,
|
|
NotificationType,
|
|
requestNotificationPermission,
|
|
} from '../store/notifications';
|
|
|
|
interface NotificationsPanelProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const TYPE_ICONS: Record<NotificationType, React.ReactNode> = {
|
|
transaction: <Send size={16} className="text-blue-400" />,
|
|
mining: <Hammer size={16} className="text-yellow-400" />,
|
|
staking: <Coins size={16} className="text-purple-400" />,
|
|
system: <Info size={16} className="text-gray-400" />,
|
|
price: <AlertCircle size={16} className="text-green-400" />,
|
|
};
|
|
|
|
export default function NotificationsPanel({ isOpen, onClose }: NotificationsPanelProps) {
|
|
const {
|
|
notifications,
|
|
preferences,
|
|
unreadCount,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
removeNotification,
|
|
clearAll,
|
|
updatePreferences,
|
|
} = useNotificationsStore();
|
|
|
|
const [showSettings, setShowSettings] = useState(false);
|
|
|
|
const handleEnableNotifications = async () => {
|
|
const granted = await requestNotificationPermission();
|
|
if (granted) {
|
|
updatePreferences({ enabled: true });
|
|
}
|
|
};
|
|
|
|
const formatTime = (timestamp: number) => {
|
|
const now = Date.now();
|
|
const diff = now - timestamp;
|
|
|
|
if (diff < 60000) return 'Just now';
|
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
return new Date(timestamp).toLocaleDateString();
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50" onClick={onClose}>
|
|
<div
|
|
className="absolute right-4 top-16 w-96 max-h-[70vh] bg-gray-900 rounded-xl border border-gray-800 shadow-xl overflow-hidden flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
|
<div className="flex items-center gap-2">
|
|
<Bell size={20} className="text-synor-400" />
|
|
<h2 className="font-semibold text-white">Notifications</h2>
|
|
{unreadCount > 0 && (
|
|
<span className="px-2 py-0.5 bg-synor-600 text-white text-xs rounded-full">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => setShowSettings(!showSettings)}
|
|
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
|
>
|
|
<Settings size={18} className="text-gray-400" />
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
|
>
|
|
<X size={18} className="text-gray-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Settings Panel */}
|
|
{showSettings && (
|
|
<div className="p-4 border-b border-gray-800 bg-gray-800/50">
|
|
<h3 className="text-sm font-medium text-white mb-3">Notification Settings</h3>
|
|
<div className="space-y-2">
|
|
<label className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-400">Enable Notifications</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={preferences.enabled}
|
|
onChange={(e) => {
|
|
if (e.target.checked) {
|
|
handleEnableNotifications();
|
|
} else {
|
|
updatePreferences({ enabled: false });
|
|
}
|
|
}}
|
|
className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-synor-600 focus:ring-synor-500"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-400">Transaction Alerts</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={preferences.transactionAlerts}
|
|
onChange={(e) =>
|
|
updatePreferences({ transactionAlerts: e.target.checked })
|
|
}
|
|
className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-synor-600 focus:ring-synor-500"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-400">Mining Alerts</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={preferences.miningAlerts}
|
|
onChange={(e) =>
|
|
updatePreferences({ miningAlerts: e.target.checked })
|
|
}
|
|
className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-synor-600 focus:ring-synor-500"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-400">Staking Alerts</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={preferences.stakingAlerts}
|
|
onChange={(e) =>
|
|
updatePreferences({ stakingAlerts: e.target.checked })
|
|
}
|
|
className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-synor-600 focus:ring-synor-500"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-400">Price Alerts</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={preferences.priceAlerts}
|
|
onChange={(e) =>
|
|
updatePreferences({ priceAlerts: e.target.checked })
|
|
}
|
|
className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-synor-600 focus:ring-synor-500"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-400">Sound</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={preferences.soundEnabled}
|
|
onChange={(e) =>
|
|
updatePreferences({ soundEnabled: e.target.checked })
|
|
}
|
|
className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-synor-600 focus:ring-synor-500"
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
{notifications.length > 0 && (
|
|
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800">
|
|
<button
|
|
onClick={markAllAsRead}
|
|
className="text-sm text-synor-400 hover:text-synor-300"
|
|
>
|
|
Mark all as read
|
|
</button>
|
|
<button
|
|
onClick={clearAll}
|
|
className="text-sm text-red-400 hover:text-red-300"
|
|
>
|
|
Clear all
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Notifications List */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
{notifications.length > 0 ? (
|
|
<div className="divide-y divide-gray-800">
|
|
{notifications.map((notification) => (
|
|
<div
|
|
key={notification.id}
|
|
className={`p-4 hover:bg-gray-800/50 transition-colors ${
|
|
!notification.read ? 'bg-synor-600/5' : ''
|
|
}`}
|
|
onClick={() => markAsRead(notification.id)}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="p-2 bg-gray-800 rounded-lg">
|
|
{TYPE_ICONS[notification.type]}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<h4
|
|
className={`font-medium ${
|
|
notification.read ? 'text-gray-400' : 'text-white'
|
|
}`}
|
|
>
|
|
{notification.title}
|
|
</h4>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
removeNotification(notification.id);
|
|
}}
|
|
className="p-1 hover:bg-gray-700 rounded transition-colors"
|
|
>
|
|
<X size={14} className="text-gray-500" />
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-0.5 line-clamp-2">
|
|
{notification.message}
|
|
</p>
|
|
<span className="text-xs text-gray-600 mt-1 block">
|
|
{formatTime(notification.timestamp)}
|
|
</span>
|
|
</div>
|
|
{!notification.read && (
|
|
<div className="w-2 h-2 bg-synor-400 rounded-full mt-2" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
|
<BellOff size={32} className="mb-3 opacity-50" />
|
|
<p>No notifications</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Bell button component to be used in the header/titlebar
|
|
export function NotificationsBell() {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const { unreadCount } = useNotificationsStore();
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => setIsOpen(true)}
|
|
className="relative p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
|
>
|
|
<Bell size={20} className="text-gray-400" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute -top-0.5 -right-0.5 w-4 h-4 bg-synor-600 text-white text-xs rounded-full flex items-center justify-center">
|
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
<NotificationsPanel isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
|
</>
|
|
);
|
|
}
|