@@ -5,17 +5,24 @@ import Link from "next/link";
55import { splitClient } from "@/lib/stellar" ;
66import { getFreighterPublicKey } from "@/lib/freighter" ;
77import InvoiceCard from "@/components/InvoiceCard" ;
8+ import BatchPayModal from "@/components/BatchPayModal" ;
89import 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 */
1315export 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