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.
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|