Skip to content

Commit 95ae672

Browse files
authored
Merge pull request #53 from vicistar-star/feat/issues-44-45-49-50
feat: batch pay, reminders, webhooks, accessibility (#44 #45 #49 #50)
2 parents b74a65c + 4514978 commit 95ae672

9 files changed

Lines changed: 673 additions & 102 deletions

File tree

src/app/dashboard/page.tsx

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@ import Link from "next/link";
55
import { splitClient } from "@/lib/stellar";
66
import { getFreighterPublicKey } from "@/lib/freighter";
77
import InvoiceCard from "@/components/InvoiceCard";
8+
import BatchPayModal from "@/components/BatchPayModal";
89
import type { Invoice } from "@stellar-split/sdk";
910

1011
/**
1112
* Dashboard — lists invoices where the connected wallet is creator or recipient.
13+
* Supports multi-select mode for batch payments.
1214
*/
1315
export default function DashboardPage() {
1416
const [publicKey, setPublicKey] = useState<string | null>(null);
1517
const [invoices, setInvoices] = useState<Invoice[]>([]);
1618
const [loading, setLoading] = useState(true);
1719
const [error, setError] = useState<string | null>(null);
1820

21+
// Multi-select state
22+
const [multiSelect, setMultiSelect] = useState(false);
23+
const [selected, setSelected] = useState<Set<string>>(new Set());
24+
const [showBatchModal, setShowBatchModal] = useState(false);
25+
1926
useEffect(() => {
2027
getFreighterPublicKey()
2128
.then(setPublicKey)
@@ -25,8 +32,6 @@ export default function DashboardPage() {
2532
useEffect(() => {
2633
if (!publicKey) return;
2734

28-
// Fetch invoices 1–50 and filter by creator or recipient.
29-
// In production this would use an indexer; here we scan a range.
3035
const fetchInvoices = async () => {
3136
setLoading(true);
3237
const results: Invoice[] = [];
@@ -37,7 +42,6 @@ export default function DashboardPage() {
3742
const isRecipient = inv.recipients.some((r) => r.address === publicKey);
3843
if (isCreator || isRecipient) results.push(inv);
3944
} catch {
40-
// Invoice doesn't exist — stop scanning.
4145
break;
4246
}
4347
}
@@ -51,6 +55,23 @@ export default function DashboardPage() {
5155
});
5256
}, [publicKey]);
5357

58+
const toggleSelect = (id: string) => {
59+
setSelected((prev) => {
60+
const next = new Set(prev);
61+
if (next.has(id)) next.delete(id);
62+
else next.add(id);
63+
return next;
64+
});
65+
};
66+
67+
const exitMultiSelect = () => {
68+
setMultiSelect(false);
69+
setSelected(new Set());
70+
};
71+
72+
const pendingInvoices = invoices.filter((inv) => inv.status === "Pending");
73+
const selectedInvoices = invoices.filter((inv) => selected.has(inv.id));
74+
5475
if (error) {
5576
return (
5677
<main className="max-w-2xl mx-auto px-6 py-20 text-center">
@@ -61,28 +82,107 @@ export default function DashboardPage() {
6182

6283
return (
6384
<main className="max-w-3xl mx-auto px-6 py-16">
64-
<div className="flex items-center justify-between mb-10">
85+
<div className="flex items-center justify-between mb-10 flex-wrap gap-3">
6586
<h1 className="text-3xl font-bold">Dashboard</h1>
66-
<Link
67-
href="/invoice/new"
68-
className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors"
69-
>
70-
+ New Invoice
71-
</Link>
87+
<div className="flex gap-2 flex-wrap">
88+
{!multiSelect && pendingInvoices.length > 0 && (
89+
<button
90+
onClick={() => setMultiSelect(true)}
91+
className="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm font-semibold transition-colors"
92+
aria-label="Enter multi-select mode to pay multiple invoices"
93+
>
94+
Pay Multiple
95+
</button>
96+
)}
97+
{multiSelect && (
98+
<>
99+
<button
100+
onClick={exitMultiSelect}
101+
className="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm font-semibold transition-colors"
102+
>
103+
Cancel
104+
</button>
105+
<button
106+
onClick={() => setShowBatchModal(true)}
107+
disabled={selected.size === 0}
108+
className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors disabled:opacity-50"
109+
aria-label={`Pay ${selected.size} selected invoice${selected.size !== 1 ? "s" : ""}`}
110+
>
111+
Pay Selected ({selected.size})
112+
</button>
113+
</>
114+
)}
115+
<Link
116+
href="/invoice/new"
117+
className="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors"
118+
>
119+
+ New Invoice
120+
</Link>
121+
</div>
72122
</div>
73123

124+
{multiSelect && (
125+
<p className="text-sm text-gray-400 mb-4" role="status">
126+
Select pending invoices to pay in a single transaction.
127+
</p>
128+
)}
129+
74130
{loading ? (
75131
<p className="text-gray-400">Loading invoices…</p>
76132
) : invoices.length === 0 ? (
77133
<p className="text-gray-400">No invoices found. Create your first one!</p>
78134
) : (
79-
<div className="flex flex-col gap-4">
80-
{invoices.map((inv) => (
81-
<Link key={inv.id} href={`/invoice/${inv.id}`}>
82-
<InvoiceCard invoice={inv} />
83-
</Link>
84-
))}
85-
</div>
135+
<ul className="flex flex-col gap-4" aria-label="Invoice list">
136+
{invoices.map((inv) => {
137+
const isSelectable = multiSelect && inv.status === "Pending";
138+
const isSelected = selected.has(inv.id);
139+
140+
return (
141+
<li key={inv.id}>
142+
{isSelectable ? (
143+
<button
144+
type="button"
145+
onClick={() => toggleSelect(inv.id)}
146+
aria-pressed={isSelected}
147+
aria-label={`${isSelected ? "Deselect" : "Select"} Invoice #${inv.id}`}
148+
className={`w-full text-left rounded-xl ring-2 transition-all ${
149+
isSelected
150+
? "ring-indigo-500"
151+
: "ring-transparent hover:ring-gray-600"
152+
}`}
153+
>
154+
<div className="relative">
155+
{isSelected && (
156+
<span
157+
aria-hidden="true"
158+
className="absolute top-3 right-3 w-5 h-5 rounded-full bg-indigo-500 flex items-center justify-center text-white text-xs font-bold z-10"
159+
>
160+
161+
</span>
162+
)}
163+
<InvoiceCard invoice={inv} />
164+
</div>
165+
</button>
166+
) : (
167+
<Link href={`/invoice/${inv.id}`} aria-label={`View Invoice #${inv.id}`}>
168+
<InvoiceCard invoice={inv} />
169+
</Link>
170+
)}
171+
</li>
172+
);
173+
})}
174+
</ul>
175+
)}
176+
177+
{showBatchModal && publicKey && selectedInvoices.length > 0 && (
178+
<BatchPayModal
179+
invoices={selectedInvoices}
180+
publicKey={publicKey}
181+
onClose={() => {
182+
setShowBatchModal(false);
183+
exitMultiSelect();
184+
}}
185+
/>
86186
)}
87187
</main>
88188
);

0 commit comments

Comments
 (0)