synor/apps/desktop-wallet/src/components/NotificationsPanel.tsx
Gulshan Yadav 63c52b26b2 feat(desktop-wallet): add comprehensive wallet features
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.
2026-02-02 09:57:55 +05:30

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)} />
</>
);
}