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 {