synor/apps/desktop-wallet/src/pages/AddressBook/AddressBookPage.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

313 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import {
Plus,
Edit2,
Trash2,
Search,
Copy,
Check,
AlertCircle,
Tag,
} from 'lucide-react';
import { useAddressBookStore, AddressBookEntry } from '../../store/addressbook';
export default function AddressBookPage() {
const {
entries,
isLoading,
error,
clearError,
fetchAll,
addEntry,
updateEntry,
deleteEntry,
} = useAddressBookStore();
const [searchQuery, setSearchQuery] = useState('');
const [showAddModal, setShowAddModal] = useState(false);
const [editingEntry, setEditingEntry] = useState<AddressBookEntry | null>(null);
const [copiedAddress, setCopiedAddress] = useState<string | null>(null);
// Form state
const [formName, setFormName] = useState('');
const [formAddress, setFormAddress] = useState('');
const [formNotes, setFormNotes] = useState('');
const [formTags, setFormTags] = useState('');
useEffect(() => {
fetchAll();
}, [fetchAll]);
const filteredEntries = entries.filter(
(entry) =>
entry.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
entry.address.toLowerCase().includes(searchQuery.toLowerCase()) ||
entry.tags.some((t) => t.toLowerCase().includes(searchQuery.toLowerCase()))
);
const resetForm = () => {
setFormName('');
setFormAddress('');
setFormNotes('');
setFormTags('');
};
const handleAdd = async () => {
if (!formName || !formAddress) return;
try {
const tags = formTags
.split(',')
.map((t) => t.trim())
.filter(Boolean);
await addEntry(formName, formAddress, formNotes || undefined, tags);
setShowAddModal(false);
resetForm();
} catch {
// Error handled by store
}
};
const handleEdit = async () => {
if (!editingEntry || !formName || !formAddress) return;
try {
const tags = formTags
.split(',')
.map((t) => t.trim())
.filter(Boolean);
await updateEntry(editingEntry.id, formName, formAddress, formNotes || undefined, tags);
setEditingEntry(null);
resetForm();
} catch {
// Error handled by store
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this contact?')) return;
try {
await deleteEntry(id);
} catch {
// Error handled by store
}
};
const copyAddress = (address: string) => {
navigator.clipboard.writeText(address);
setCopiedAddress(address);
setTimeout(() => setCopiedAddress(null), 2000);
};
const openEditModal = (entry: AddressBookEntry) => {
setEditingEntry(entry);
setFormName(entry.name);
setFormAddress(entry.address);
setFormNotes(entry.notes || '');
setFormTags(entry.tags.join(', '));
};
const renderEntryCard = (entry: AddressBookEntry) => (
<div
key={entry.id}
className="bg-gray-900 rounded-xl p-4 border border-gray-800 hover:border-gray-700 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-white truncate">{entry.name}</h3>
</div>
<div className="flex items-center gap-2 mt-1">
<code className="text-sm text-gray-400 truncate">{entry.address}</code>
<button
onClick={() => copyAddress(entry.address)}
className="p-1 hover:bg-gray-800 rounded transition-colors"
>
{copiedAddress === entry.address ? (
<Check size={14} className="text-green-400" />
) : (
<Copy size={14} className="text-gray-500" />
)}
</button>
</div>
{entry.notes && (
<p className="text-sm text-gray-500 mt-2 line-clamp-2">{entry.notes}</p>
)}
{entry.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{entry.tags.map((tag, i) => (
<span
key={i}
className="px-2 py-0.5 bg-synor-600/20 text-synor-400 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex items-center gap-1 ml-4">
<button
onClick={() => openEditModal(entry)}
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
>
<Edit2 size={18} className="text-gray-400" />
</button>
<button
onClick={() => handleDelete(entry.id)}
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
>
<Trash2 size={18} className="text-red-400" />
</button>
</div>
</div>
</div>
);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Address Book</h1>
<p className="text-gray-400 mt-1">Manage your saved addresses</p>
</div>
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors"
>
<Plus size={18} />
Add Contact
</button>
</div>
{/* Error Alert */}
{error && (
<div className="flex items-center gap-3 p-4 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="text-red-400" size={20} />
<p className="text-red-400 flex-1">{error}</p>
<button onClick={clearError} className="text-red-400 hover:text-red-300">
×
</button>
</div>
)}
{/* Search */}
<div className="relative">
<Search
size={18}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by name, address, or tag..."
className="w-full pl-10 pr-4 py-3 bg-gray-900 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
{/* All Contacts */}
<div>
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-3">
All Contacts ({filteredEntries.length})
</h2>
{filteredEntries.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredEntries.map(renderEntryCard)}
</div>
) : (
<div className="text-center py-12 text-gray-500">
{searchQuery ? 'No contacts found' : 'No contacts yet'}
</div>
)}
</div>
{/* Add/Edit Modal */}
{(showAddModal || editingEntry) && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-xl p-6 w-full max-w-md border border-gray-800">
<h2 className="text-xl font-bold text-white mb-4">
{editingEntry ? 'Edit Contact' : 'Add Contact'}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Name *</label>
<input
type="text"
value={formName}
onChange={(e) => setFormName(e.target.value)}
placeholder="Contact name"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Address *</label>
<input
type="text"
value={formAddress}
onChange={(e) => setFormAddress(e.target.value)}
placeholder="synor1..."
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono text-sm placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">
Tags (comma separated)
</label>
<div className="relative">
<Tag
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
/>
<input
type="text"
value={formTags}
onChange={(e) => setFormTags(e.target.value)}
placeholder="e.g., Exchange, Friend, Business"
className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Notes</label>
<textarea
value={formNotes}
onChange={(e) => setFormNotes(e.target.value)}
placeholder="Optional notes about this contact"
rows={3}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500 resize-none"
/>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => {
setShowAddModal(false);
setEditingEntry(null);
resetForm();
}}
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-gray-300 font-medium transition-colors"
>
Cancel
</button>
<button
onClick={editingEntry ? handleEdit : handleAdd}
disabled={!formName || !formAddress || isLoading}
className="flex-1 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
{isLoading ? 'Saving...' : editingEntry ? 'Save Changes' : 'Add Contact'}
</button>
</div>
</div>
</div>
)}
</div>
);
}