Preview
Full width desktop view
Code
banking-3.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { motion, AnimatePresence } from "framer-motion";
6import {
7 Calendar,
8 Clock,
9 CreditCard,
10 DollarSign,
11 Eye,
12 Filter,
13 Search,
14 ArrowUpDown,
15 CheckCircle,
16 XCircle,
17 AlertCircle,
18 Loader2,
19} from "lucide-react";
20import { cn } from "@/lib/utils";
21
22// Card components
23const Card = React.forwardRef<
24 HTMLDivElement,
25 React.HTMLAttributes<HTMLDivElement>
26>(({ className, ...props }, ref) => (
27 <div
28 ref={ref}
29 className={cn(
30 "rounded-lg border bg-card text-card-foreground shadow-sm",
31 className
32 )}
33 {...props}
34 />
35));
36Card.displayName = "Card";
37
38const CardContent = React.forwardRef<
39 HTMLDivElement,
40 React.HTMLAttributes<HTMLDivElement>
41>(({ className, ...props }, ref) => (
42 <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
43));
44CardContent.displayName = "CardContent";
45
46// Input component
47const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
48 ({ className, type, ...props }, ref) => {
49 return (
50 <input
51 type={type}
52 className={cn(
53 "flex h-9 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm shadow-black/5 transition-shadow placeholder:text-muted-foreground/70 focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50",
54 className
55 )}
56 ref={ref}
57 {...props}
58 />
59 );
60 }
61);
62Input.displayName = "Input";
63
64// Button component
65const Button = React.forwardRef<
66 HTMLButtonElement,
67 React.ButtonHTMLAttributes<HTMLButtonElement> & {
68 variant?: "default" | "outline" | "ghost";
69 size?: "default" | "sm" | "lg";
70 }
71>(({ className, variant = "default", size = "default", ...props }, ref) => {
72 return (
73 <button
74 className={cn(
75 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
76 variant === "default" &&
77 "bg-primary text-primary-foreground shadow hover:bg-primary/90",
78 variant === "outline" &&
79 "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
80 variant === "ghost" && "hover:bg-accent hover:text-accent-foreground",
81 size === "default" && "h-9 px-4 py-2",
82 size === "sm" && "h-8 rounded-md px-3 text-xs",
83 size === "lg" && "h-10 rounded-md px-8",
84 className
85 )}
86 ref={ref}
87 {...props}
88 />
89 );
90});
91Button.displayName = "Button";
92
93// Badge component
94const Badge = React.forwardRef<
95 HTMLDivElement,
96 React.HTMLAttributes<HTMLDivElement> & {
97 variant?: "default" | "success" | "warning" | "destructive" | "secondary";
98 }
99>(({ className, variant = "default", ...props }, ref) => {
100 return (
101 <div
102 ref={ref}
103 className={cn(
104 "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
105 variant === "default" &&
106 "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
107 variant === "success" &&
108 "border-transparent bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
109 variant === "warning" &&
110 "border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
111 variant === "destructive" &&
112 "border-transparent bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
113 variant === "secondary" &&
114 "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
115 className
116 )}
117 {...props}
118 />
119 );
120});
121Badge.displayName = "Badge";
122
123// Types
124interface MoneyTransferTransaction {
125 id: string;
126 amount: number;
127 currency: string;
128 destination: {
129 name: string;
130 accountNumber: string;
131 bankName: string;
132 };
133 paymentDate: Date;
134 disbursementDate: Date | null;
135 paymentStatus: "pending" | "completed" | "failed";
136 disbursementStatus: "pending" | "processing" | "completed" | "failed";
137 paymentProcessor: string;
138 platformFee: number;
139 paymentFee: number;
140 disbursementFee: number;
141 transactionReference: string;
142}
143
144interface MoneyTransferListProps {
145 transactions?: MoneyTransferTransaction[];
146 onViewDetails?: (transactionId: string) => void;
147 className?: string;
148}
149
150// Default data
151const defaultTransactions: MoneyTransferTransaction[] = [
152 {
153 id: "TXN-001",
154 amount: 1500000,
155 currency: "IDR",
156 destination: {
157 name: "John Doe",
158 accountNumber: "1234567890",
159 bankName: "Bank Central Asia",
160 },
161 paymentDate: new Date("2024-01-15T10:30:00"),
162 disbursementDate: new Date("2024-01-15T10:35:00"),
163 paymentStatus: "completed",
164 disbursementStatus: "completed",
165 paymentProcessor: "Midtrans",
166 platformFee: 15000,
167 paymentFee: 5000,
168 disbursementFee: 2500,
169 transactionReference: "REF-001-2024",
170 },
171 {
172 id: "TXN-002",
173 amount: 750000,
174 currency: "IDR",
175 destination: {
176 name: "Jane Smith",
177 accountNumber: "0987654321",
178 bankName: "Bank Mandiri",
179 },
180 paymentDate: new Date("2024-01-15T14:20:00"),
181 disbursementDate: null,
182 paymentStatus: "completed",
183 disbursementStatus: "processing",
184 paymentProcessor: "Xendit",
185 platformFee: 7500,
186 paymentFee: 3000,
187 disbursementFee: 2000,
188 transactionReference: "REF-002-2024",
189 },
190 {
191 id: "TXN-003",
192 amount: 2250000,
193 currency: "IDR",
194 destination: {
195 name: "Robert Johnson",
196 accountNumber: "5678901234",
197 bankName: "Bank Negara Indonesia",
198 },
199 paymentDate: new Date("2024-01-15T16:45:00"),
200 disbursementDate: new Date("2024-01-15T16:50:00"),
201 paymentStatus: "completed",
202 disbursementStatus: "completed",
203 paymentProcessor: "OVO",
204 platformFee: 22500,
205 paymentFee: 7500,
206 disbursementFee: 3000,
207 transactionReference: "REF-003-2024",
208 },
209 {
210 id: "TXN-004",
211 amount: 500000,
212 currency: "IDR",
213 destination: {
214 name: "Sarah Wilson",
215 accountNumber: "3456789012",
216 bankName: "Bank Rakyat Indonesia",
217 },
218 paymentDate: new Date("2024-01-15T18:15:00"),
219 disbursementDate: null,
220 paymentStatus: "failed",
221 disbursementStatus: "pending",
222 paymentProcessor: "GoPay",
223 platformFee: 5000,
224 paymentFee: 2500,
225 disbursementFee: 1500,
226 transactionReference: "REF-004-2024",
227 },
228];
229
230const MoneyTransferList: React.FC<MoneyTransferListProps> = ({
231 transactions = defaultTransactions,
232 onViewDetails,
233 className,
234}) => {
235 const [searchTerm, setSearchTerm] = useState("");
236 const [statusFilter, setStatusFilter] = useState<string>("all");
237 const [sortBy, setSortBy] = useState<"date" | "amount">("date");
238 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
239
240 const getStatusIcon = (status: string) => {
241 switch (status) {
242 case "completed":
243 return <CheckCircle className="w-4 h-4 text-green-500" />;
244 case "failed":
245 return <XCircle className="w-4 h-4 text-red-500" />;
246 case "processing":
247 return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
248 case "pending":
249 return <AlertCircle className="w-4 h-4 text-yellow-500" />;
250 default:
251 return <AlertCircle className="w-4 h-4 text-gray-500" />;
252 }
253 };
254
255 const getStatusBadge = (status: string) => {
256 switch (status) {
257 case "completed":
258 return <Badge variant="success">Completed</Badge>;
259 case "failed":
260 return <Badge variant="destructive">Failed</Badge>;
261 case "processing":
262 return <Badge variant="default">Processing</Badge>;
263 case "pending":
264 return <Badge variant="warning">Pending</Badge>;
265 default:
266 return <Badge variant="secondary">Unknown</Badge>;
267 }
268 };
269
270 const formatCurrency = (amount: number, currency: string) => {
271 return new Intl.NumberFormat("id-ID", {
272 style: "currency",
273 currency: currency,
274 minimumFractionDigits: 0,
275 }).format(amount);
276 };
277
278 const formatDateTime = (date: Date | null) => {
279 if (!date) return "N/A";
280 return new Intl.DateTimeFormat("id-ID", {
281 year: "numeric",
282 month: "short",
283 day: "2-digit",
284 hour: "2-digit",
285 minute: "2-digit",
286 }).format(date);
287 };
288
289 const filteredAndSortedTransactions = React.useMemo(() => {
290 let filtered = transactions.filter((transaction) => {
291 const matchesSearch =
292 transaction.destination.name
293 .toLowerCase()
294 .includes(searchTerm.toLowerCase()) ||
295 transaction.transactionReference
296 .toLowerCase()
297 .includes(searchTerm.toLowerCase()) ||
298 transaction.destination.bankName
299 .toLowerCase()
300 .includes(searchTerm.toLowerCase());
301
302 const matchesStatus =
303 statusFilter === "all" ||
304 transaction.paymentStatus === statusFilter ||
305 transaction.disbursementStatus === statusFilter;
306
307 return matchesSearch && matchesStatus;
308 });
309
310 filtered.sort((a, b) => {
311 let comparison = 0;
312 if (sortBy === "date") {
313 comparison = a.paymentDate.getTime() - b.paymentDate.getTime();
314 } else if (sortBy === "amount") {
315 comparison = a.amount - b.amount;
316 }
317 return sortOrder === "asc" ? comparison : -comparison;
318 });
319
320 return filtered;
321 }, [transactions, searchTerm, statusFilter, sortBy, sortOrder]);
322
323 const handleSort = (field: "date" | "amount") => {
324 if (sortBy === field) {
325 setSortOrder(sortOrder === "asc" ? "desc" : "asc");
326 } else {
327 setSortBy(field);
328 setSortOrder("desc");
329 }
330 };
331
332 return (
333 <div className={cn("w-full space-y-6", className)}>
334 {/* Header */}
335 <div className="flex flex-col space-y-4">
336 <div className="flex items-center justify-between">
337 <h2 className="text-2xl font-bold text-foreground">
338 Money Transfer Transactions
339 </h2>
340 <div className="text-sm text-muted-foreground">
341 {filteredAndSortedTransactions.length} transactions
342 </div>
343 </div>
344
345 {/* Filters and Search */}
346 <div className="flex flex-col sm:flex-row gap-4">
347 <div className="relative flex-1">
348 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
349 <Input
350 placeholder="Search by recipient, reference, or bank..."
351 value={searchTerm}
352 onChange={(e) => setSearchTerm(e.target.value)}
353 className="pl-10"
354 />
355 </div>
356
357 <div className="flex gap-2">
358 <select
359 value={statusFilter}
360 onChange={(e) => setStatusFilter(e.target.value)}
361 className="px-3 py-2 border border-input bg-background rounded-lg text-sm"
362 >
363 <option value="all">All Status</option>
364 <option value="completed">Completed</option>
365 <option value="pending">Pending</option>
366 <option value="processing">Processing</option>
367 <option value="failed">Failed</option>
368 </select>
369
370 <Button
371 variant="outline"
372 size="sm"
373 onClick={() => handleSort("date")}
374 className="flex items-center gap-2"
375 >
376 <Calendar className="w-4 h-4" />
377 Date
378 <ArrowUpDown className="w-3 h-3" />
379 </Button>
380
381 <Button
382 variant="outline"
383 size="sm"
384 onClick={() => handleSort("amount")}
385 className="flex items-center gap-2"
386 >
387 <DollarSign className="w-4 h-4" />
388 Amount
389 <ArrowUpDown className="w-3 h-3" />
390 </Button>
391 </div>
392 </div>
393 </div>
394
395 {/* Transaction List */}
396 <div className="space-y-4">
397 <AnimatePresence>
398 {filteredAndSortedTransactions.map((transaction, index) => (
399 <motion.div
400 key={transaction.id}
401 initial={{ opacity: 0, y: 20 }}
402 animate={{ opacity: 1, y: 0 }}
403 exit={{ opacity: 0, y: -20 }}
404 transition={{ delay: index * 0.05 }}
405 >
406 <Card className="hover:shadow-md transition-shadow">
407 <CardContent className="p-6">
408 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
409 {/* Transaction Info */}
410 <div className="space-y-4">
411 <div className="flex items-start justify-between">
412 <div>
413 <h3 className="font-semibold text-foreground">
414 {transaction.destination.name}
415 </h3>
416 <p className="text-sm text-muted-foreground">
417 {transaction.destination.bankName}
418 </p>
419 <p className="text-sm text-muted-foreground">
420 Account: {transaction.destination.accountNumber}
421 </p>
422 </div>
423 <div className="text-right">
424 <p className="text-lg font-bold text-foreground">
425 {formatCurrency(
426 transaction.amount,
427 transaction.currency
428 )}
429 </p>
430 <p className="text-xs text-muted-foreground">
431 {transaction.transactionReference}
432 </p>
433 </div>
434 </div>
435
436 <div className="flex items-center gap-2 text-sm text-muted-foreground">
437 <CreditCard className="w-4 h-4" />
438 <span>{transaction.paymentProcessor}</span>
439 </div>
440 </div>
441
442 {/* Status and Dates */}
443 <div className="space-y-4">
444 <div className="space-y-3">
445 <div className="flex items-center justify-between">
446 <span className="text-sm font-medium">
447 Payment Status
448 </span>
449 <div className="flex items-center gap-2">
450 {getStatusIcon(transaction.paymentStatus)}
451 {getStatusBadge(transaction.paymentStatus)}
452 </div>
453 </div>
454
455 <div className="flex items-center justify-between">
456 <span className="text-sm font-medium">
457 Disbursement Status
458 </span>
459 <div className="flex items-center gap-2">
460 {getStatusIcon(transaction.disbursementStatus)}
461 {getStatusBadge(transaction.disbursementStatus)}
462 </div>
463 </div>
464 </div>
465
466 <div className="space-y-2 text-sm">
467 <div className="flex items-center gap-2">
468 <Clock className="w-4 h-4 text-muted-foreground" />
469 <span className="text-muted-foreground">
470 Payment:
471 </span>
472 <span>{formatDateTime(transaction.paymentDate)}</span>
473 </div>
474 <div className="flex items-center gap-2">
475 <Clock className="w-4 h-4 text-muted-foreground" />
476 <span className="text-muted-foreground">
477 Disbursement:
478 </span>
479 <span>
480 {formatDateTime(transaction.disbursementDate)}
481 </span>
482 </div>
483 </div>
484 </div>
485
486 {/* Fees and Actions */}
487 <div className="space-y-4">
488 <div className="space-y-2">
489 <h4 className="text-sm font-medium text-foreground">
490 Fee Breakdown
491 </h4>
492 <div className="space-y-1 text-sm">
493 <div className="flex justify-between">
494 <span className="text-muted-foreground">
495 Platform Fee:
496 </span>
497 <span className="font-medium text-green-600">
498 {formatCurrency(
499 transaction.platformFee,
500 transaction.currency
501 )}
502 </span>
503 </div>
504 <div className="flex justify-between">
505 <span className="text-muted-foreground">
506 Payment Fee:
507 </span>
508 <span>
509 {formatCurrency(
510 transaction.paymentFee,
511 transaction.currency
512 )}
513 </span>
514 </div>
515 <div className="flex justify-between">
516 <span className="text-muted-foreground">
517 Disbursement Fee:
518 </span>
519 <span>
520 {formatCurrency(
521 transaction.disbursementFee,
522 transaction.currency
523 )}
524 </span>
525 </div>
526 </div>
527 </div>
528
529 <Button
530 variant="outline"
531 size="sm"
532 onClick={() => onViewDetails?.(transaction.id)}
533 className="w-full flex items-center gap-2"
534 >
535 <Eye className="w-4 h-4" />
536 View Details
537 </Button>
538 </div>
539 </div>
540 </CardContent>
541 </Card>
542 </motion.div>
543 ))}
544 </AnimatePresence>
545
546 {filteredAndSortedTransactions.length === 0 && (
547 <div className="text-center py-12">
548 <div className="text-muted-foreground">
549 <Filter className="w-12 h-12 mx-auto mb-4 opacity-50" />
550 <p className="text-lg font-medium">No transactions found</p>
551 <p className="text-sm">
552 Try adjusting your search or filter criteria
553 </p>
554 </div>
555 </div>
556 )}
557 </div>
558 </div>
559 );
560};
561
562// Usage example
563export default function MoneyTransferDemo() {
564 const handleViewDetails = (transactionId: string) => {
565 console.log("Viewing details for transaction:", transactionId);
566 };
567
568 return (
569 <div className="min-h-screen bg-background p-6">
570 <div className="max-w-7xl mx-auto">
571 <MoneyTransferList onViewDetails={handleViewDetails} />
572 </div>
573 </div>
574 );
575}
Dependencies
External Libraries
framer-motionlucide-reactreact
Local Components
/lib/utils