Changes
gpt-engineer-app[bot] committed
Nov 27, 2025 at 11:40 UTC
4f21c62ffe9dc64ea8740339a9b7adf162073c6c
5 files changed
+354
-4
src/App.tsx
+2
@@ -6,6 +6,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
6
import Home from "./pages/Home";
7
import Pay from "./pages/Pay";
8
import SendMoney from "./pages/SendMoney";
9
+import Recipients from "./pages/Recipients";
10
import Profile from "./pages/Profile";
11
import NotFound from "./pages/NotFound";
12
@@ -21,6 +22,7 @@ const App = () => (
22
<Route path="/" element={<Home />} />
23
<Route path="/pay" element={<Pay />} />
24
<Route path="/send" element={<SendMoney />} />
25
+ <Route path="/recipients" element={<Recipients />} />
26
<Route path="/profile" element={<Profile />} />
27
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
28
<Route path="*" element={<NotFound />} />
src/pages/Pay.tsx
+1
-1
@@ -69,7 +69,7 @@ const Pay = () => {
69
<Card
70
key={action.id}
71
className="p-6 hover:shadow-lg transition-all cursor-pointer border-0 shadow-md active:scale-95"
72
- onClick={() => action.route === "/send" ? navigate(action.route) : null}
72
+ onClick={() => navigate(action.route)}
73
>
74
<div className="space-y-4">
75
<action.icon className={`h-10 w-10 ${action.color}`} />
src/pages/Recipients.tsx
new
+311
@@ -0,0 +1,311 @@
1
+import { useState, useEffect } from "react";
2
+import { useNavigate } from "react-router-dom";
3
+import Layout from "@/components/Layout";
4
+import { Card } from "@/components/ui/card";
5
+import { Button } from "@/components/ui/button";
6
+import { Input } from "@/components/ui/input";
7
+import { Label } from "@/components/ui/label";
8
+import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+} from "@/components/ui/dialog";
16
+import {
17
+ AlertDialog,
18
+ AlertDialogAction,
19
+ AlertDialogCancel,
20
+ AlertDialogContent,
21
+ AlertDialogDescription,
22
+ AlertDialogFooter,
23
+ AlertDialogHeader,
24
+ AlertDialogTitle,
25
+} from "@/components/ui/alert-dialog";
26
+import { Plus, Pencil, Trash2, Send } from "lucide-react";
27
+import { transactionService } from "@/services/transactionService";
28
+import { Recipient } from "@/types/transaction";
29
+import { toast } from "@/hooks/use-toast";
30
+
31
+const Recipients = () => {
32
+ const navigate = useNavigate();
33
+ const [recipients, setRecipients] = useState<Recipient[]>([]);
34
+ const [loading, setLoading] = useState(true);
35
+ const [dialogOpen, setDialogOpen] = useState(false);
36
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
37
+ const [editingRecipient, setEditingRecipient] = useState<Recipient | null>(null);
38
+ const [deletingRecipient, setDeletingRecipient] = useState<Recipient | null>(null);
39
+ const [name, setName] = useState("");
40
+ const [email, setEmail] = useState("");
41
+ const [saving, setSaving] = useState(false);
42
+
43
+ useEffect(() => {
44
+ loadRecipients();
45
+ }, []);
46
+
47
+ const loadRecipients = async () => {
48
+ setLoading(true);
49
+ const data = await transactionService.getRecipients();
50
+ setRecipients(data);
51
+ setLoading(false);
52
+ };
53
+
54
+ const handleOpenDialog = (recipient?: Recipient) => {
55
+ if (recipient) {
56
+ setEditingRecipient(recipient);
57
+ setName(recipient.name);
58
+ setEmail(recipient.email);
59
+ } else {
60
+ setEditingRecipient(null);
61
+ setName("");
62
+ setEmail("");
63
+ }
64
+ setDialogOpen(true);
65
+ };
66
+
67
+ const handleCloseDialog = () => {
68
+ setDialogOpen(false);
69
+ setEditingRecipient(null);
70
+ setName("");
71
+ setEmail("");
72
+ };
73
+
74
+ const handleSave = async () => {
75
+ if (!name.trim() || !email.trim()) {
76
+ toast({
77
+ title: "Missing Information",
78
+ description: "Please fill in all fields",
79
+ variant: "destructive",
80
+ });
81
+ return;
82
+ }
83
+
84
+ // Basic email validation
85
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
86
+ if (!emailRegex.test(email)) {
87
+ toast({
88
+ title: "Invalid Email",
89
+ description: "Please enter a valid email address",
90
+ variant: "destructive",
91
+ });
92
+ return;
93
+ }
94
+
95
+ setSaving(true);
96
+ try {
97
+ if (editingRecipient) {
98
+ await transactionService.updateRecipient(editingRecipient.id, name, email);
99
+ toast({
100
+ title: "Recipient Updated",
101
+ description: `${name} has been updated successfully`,
102
+ });
103
+ } else {
104
+ await transactionService.addRecipient(name, email);
105
+ toast({
106
+ title: "Recipient Added",
107
+ description: `${name} has been added to your recipients`,
108
+ });
109
+ }
110
+ await loadRecipients();
111
+ handleCloseDialog();
112
+ } catch (error) {
113
+ toast({
114
+ title: "Error",
115
+ description: "Failed to save recipient. Please try again.",
116
+ variant: "destructive",
117
+ });
118
+ } finally {
119
+ setSaving(false);
120
+ }
121
+ };
122
+
123
+ const handleDelete = async () => {
124
+ if (!deletingRecipient) return;
125
+
126
+ try {
127
+ await transactionService.deleteRecipient(deletingRecipient.id);
128
+ toast({
129
+ title: "Recipient Deleted",
130
+ description: `${deletingRecipient.name} has been removed`,
131
+ });
132
+ await loadRecipients();
133
+ setDeleteDialogOpen(false);
134
+ setDeletingRecipient(null);
135
+ } catch (error) {
136
+ toast({
137
+ title: "Error",
138
+ description: "Failed to delete recipient. Please try again.",
139
+ variant: "destructive",
140
+ });
141
+ }
142
+ };
143
+
144
+ const openDeleteDialog = (recipient: Recipient) => {
145
+ setDeletingRecipient(recipient);
146
+ setDeleteDialogOpen(true);
147
+ };
148
+
149
+ return (
150
+ <Layout title="Recipients">
151
+ <div className="p-4 space-y-6">
152
+ {/* Header */}
153
+ <div className="flex items-center justify-between">
154
+ <div>
155
+ <h2 className="text-2xl font-bold">Payment Recipients</h2>
156
+ <p className="text-sm text-muted-foreground font-medium mt-1">
157
+ Manage your frequent payment contacts
158
+ </p>
159
+ </div>
160
+ <Button onClick={() => handleOpenDialog()}>
161
+ <Plus className="h-5 w-5 mr-2" />
162
+ Add
163
+ </Button>
164
+ </div>
165
+
166
+ {/* Recipients List */}
167
+ {loading ? (
168
+ <Card className="p-8 border-0 shadow-md text-center">
169
+ <p className="text-muted-foreground">Loading recipients...</p>
170
+ </Card>
171
+ ) : recipients.length === 0 ? (
172
+ <Card className="p-12 border-0 shadow-md text-center space-y-4">
173
+ <div className="h-20 w-20 rounded-full bg-muted mx-auto flex items-center justify-center">
174
+ <Plus className="h-10 w-10 text-muted-foreground" />
175
+ </div>
176
+ <div>
177
+ <h3 className="text-lg font-bold mb-2">No Recipients Yet</h3>
178
+ <p className="text-sm text-muted-foreground font-medium mb-4">
179
+ Add your first recipient to get started
180
+ </p>
181
+ <Button onClick={() => handleOpenDialog()}>
182
+ <Plus className="h-5 w-5 mr-2" />
183
+ Add Recipient
184
+ </Button>
185
+ </div>
186
+ </Card>
187
+ ) : (
188
+ <div className="space-y-3">
189
+ {recipients.map((recipient) => (
190
+ <Card
191
+ key={recipient.id}
192
+ className="p-5 border-0 shadow-md hover:shadow-lg transition-all"
193
+ >
194
+ <div className="flex items-center gap-4">
195
+ <div className="h-14 w-14 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
196
+ <span className="text-xl font-bold text-primary">
197
+ {recipient.name.charAt(0).toUpperCase()}
198
+ </span>
199
+ </div>
200
+ <div className="flex-1 min-w-0">
201
+ <p className="font-semibold text-base truncate">{recipient.name}</p>
202
+ <p className="text-sm text-muted-foreground font-medium truncate">
203
+ {recipient.email}
204
+ </p>
205
+ </div>
206
+ <div className="flex items-center gap-2 flex-shrink-0">
207
+ <Button
208
+ size="icon"
209
+ variant="ghost"
210
+ onClick={() => navigate("/send", { state: { recipient } })}
211
+ className="h-10 w-10"
212
+ >
213
+ <Send className="h-5 w-5" />
214
+ </Button>
215
+ <Button
216
+ size="icon"
217
+ variant="ghost"
218
+ onClick={() => handleOpenDialog(recipient)}
219
+ className="h-10 w-10"
220
+ >
221
+ <Pencil className="h-5 w-5" />
222
+ </Button>
223
+ <Button
224
+ size="icon"
225
+ variant="ghost"
226
+ onClick={() => openDeleteDialog(recipient)}
227
+ className="h-10 w-10 text-destructive hover:text-destructive"
228
+ >
229
+ <Trash2 className="h-5 w-5" />
230
+ </Button>
231
+ </div>
232
+ </div>
233
+ </Card>
234
+ ))}
235
+ </div>
236
+ )}
237
+ </div>
238
+
239
+ {/* Add/Edit Dialog */}
240
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
241
+ <DialogContent>
242
+ <DialogHeader>
243
+ <DialogTitle>
244
+ {editingRecipient ? "Edit Recipient" : "Add Recipient"}
245
+ </DialogTitle>
246
+ <DialogDescription>
247
+ {editingRecipient
248
+ ? "Update the recipient's information"
249
+ : "Add a new recipient to your contacts"}
250
+ </DialogDescription>
251
+ </DialogHeader>
252
+ <div className="space-y-4 py-4">
253
+ <div className="space-y-2">
254
+ <Label htmlFor="name">Name</Label>
255
+ <Input
256
+ id="name"
257
+ placeholder="John Doe"
258
+ value={name}
259
+ onChange={(e) => setName(e.target.value)}
260
+ />
261
+ </div>
262
+ <div className="space-y-2">
263
+ <Label htmlFor="email">Email Address</Label>
264
+ <Input
265
+ id="email"
266
+ type="email"
267
+ placeholder="john@example.com"
268
+ value={email}
269
+ onChange={(e) => setEmail(e.target.value)}
270
+ />
271
+ </div>
272
+ </div>
273
+ <DialogFooter>
274
+ <Button variant="outline" onClick={handleCloseDialog}>
275
+ Cancel
276
+ </Button>
277
+ <Button onClick={handleSave} disabled={saving}>
278
+ {saving ? "Saving..." : editingRecipient ? "Update" : "Add"}
279
+ </Button>
280
+ </DialogFooter>
281
+ </DialogContent>
282
+ </Dialog>
283
+
284
+ {/* Delete Confirmation Dialog */}
285
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
286
+ <AlertDialogContent>
287
+ <AlertDialogHeader>
288
+ <AlertDialogTitle>Delete Recipient?</AlertDialogTitle>
289
+ <AlertDialogDescription>
290
+ Are you sure you want to delete {deletingRecipient?.name}? This action
291
+ cannot be undone.
292
+ </AlertDialogDescription>
293
+ </AlertDialogHeader>
294
+ <AlertDialogFooter>
295
+ <AlertDialogCancel onClick={() => setDeletingRecipient(null)}>
296
+ Cancel
297
+ </AlertDialogCancel>
298
+ <AlertDialogAction
299
+ onClick={handleDelete}
300
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
301
+ >
302
+ Delete
303
+ </AlertDialogAction>
304
+ </AlertDialogFooter>
305
+ </AlertDialogContent>
306
+ </AlertDialog>
307
+ </Layout>
308
+ );
309
+};
310
+
311
+export default Recipients;
src/pages/SendMoney.tsx
+12
-2
@@ -1,5 +1,5 @@
1
-import { useState } from "react";
2
-import { useNavigate } from "react-router-dom";
1
+import { useState, useEffect } from "react";
2
+import { useNavigate, useLocation } from "react-router-dom";
3
import Layout from "@/components/Layout";
4
import { Card } from "@/components/ui/card";
5
import { Input } from "@/components/ui/input";
@@ -13,6 +13,7 @@ import { toast } from "@/hooks/use-toast";
13
14
const SendMoney = () => {
15
const navigate = useNavigate();
16
+ const location = useLocation();
17
const [step, setStep] = useState<"form" | "processing" | "success">("form");
18
const [amount, setAmount] = useState("");
19
const [currency, setCurrency] = useState<Currency>("USD");
@@ -21,6 +22,15 @@ const SendMoney = () => {
22
const [description, setDescription] = useState("");
23
const [transactionId, setTransactionId] = useState("");
24
25
+ // Pre-fill recipient data if passed from Recipients page
26
+ useEffect(() => {
27
+ if (location.state?.recipient) {
28
+ const { name, email } = location.state.recipient;
29
+ setRecipientName(name);
30
+ setRecipient(email);
31
+ }
32
+ }, [location]);
33
+
34
const fee = amount ? transactionService.calculateFee(parseFloat(amount)) : 0;
35
const total = amount ? parseFloat(amount) + fee : 0;
36
src/services/transactionService.ts
+28
-1
@@ -70,6 +70,7 @@ const exchangeRates: Record<Currency, number> = {
70
71
class TransactionService {
72
private transactions: Transaction[] = [...mockTransactions];
73
+ private recipients: Recipient[] = [...mockRecipients];
74
75
async sendMoney(
76
amount: number,
@@ -124,7 +125,33 @@ class TransactionService {
125
126
async getRecipients(): Promise<Recipient[]> {
127
await new Promise(resolve => setTimeout(resolve, 200));
127
- return [...mockRecipients];
128
+ return [...this.recipients];
129
+ }
130
+
131
+ async addRecipient(name: string, email: string): Promise<Recipient> {
132
+ await new Promise(resolve => setTimeout(resolve, 300));
133
+ const newRecipient: Recipient = {
134
+ id: `rec_${Date.now()}`,
135
+ name,
136
+ email,
137
+ };
138
+ this.recipients.push(newRecipient);
139
+ return newRecipient;
140
+ }
141
+
142
+ async updateRecipient(id: string, name: string, email: string): Promise<Recipient> {
143
+ await new Promise(resolve => setTimeout(resolve, 300));
144
+ const recipientIndex = this.recipients.findIndex(r => r.id === id);
145
+ if (recipientIndex === -1) {
146
+ throw new Error("Recipient not found");
147
+ }
148
+ this.recipients[recipientIndex] = { id, name, email };
149
+ return this.recipients[recipientIndex];
150
+ }
151
+
152
+ async deleteRecipient(id: string): Promise<void> {
153
+ await new Promise(resolve => setTimeout(resolve, 300));
154
+ this.recipients = this.recipients.filter(r => r.id !== id);
155
}
156
157
calculateFee(amount: number): number {