ShadcnUI Vaults

Back to Blocks

Animated Transaction List

Unknown Block

UnknownComponent

Animated Transaction List

Modern animated transaction list with smooth transitions, status indicators, and detailed fee breakdown.

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

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.