From 64f1354cf1276b68c8e12cf78fdaca8e871cd38a Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Sat, 8 Mar 2025 11:36:56 +0700 Subject: [PATCH 001/107] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a64d7ac..23f708e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # CryptoPath - Path Your Crypto Future **COS30049 - Computing Technology Innovation Project** -## Installation Guide +## Installation Guide - duy deo trai ### Prerequisites - Node.js 18.0 or higher From 3a27c775b27c9fbfa6cb7f4b260f9707985a72d7 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Sat, 8 Mar 2025 11:53:42 +0700 Subject: [PATCH 002/107] add graphs --- components/TransactionTable.tsx | 3 +- components/ui/NetworkStats.tsx | 2 +- components/ui/RevenueGraph.tsx | 85 ++++++++++ components/ui/WalletCharts.tsx | 277 ++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 components/ui/RevenueGraph.tsx create mode 100644 components/ui/WalletCharts.tsx diff --git a/components/TransactionTable.tsx b/components/TransactionTable.tsx index a52d878..16e8c9b 100644 --- a/components/TransactionTable.tsx +++ b/components/TransactionTable.tsx @@ -8,6 +8,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Button } from "@/components/ui/button" import { Loader2 } from "lucide-react" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { TransactionTableProps } from '@/lib/types' interface Transaction { id: string @@ -18,7 +19,7 @@ interface Transaction { type: "transfer" | "swap" | "inflow" | "outflow" } -export default function TransactionTable() { +export default function TransactionTable({ data }: TransactionTableProps) { const searchParams = useSearchParams() const address = searchParams.get("address") const [transactions, setTransactions] = useState([]) diff --git a/components/ui/NetworkStats.tsx b/components/ui/NetworkStats.tsx index 4b8aecb..421402e 100644 --- a/components/ui/NetworkStats.tsx +++ b/components/ui/NetworkStats.tsx @@ -33,7 +33,7 @@ export default function TransactionExplorer() { // Etherscan API configuration const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key - const API_URL = `/api/etherscan?module=proxy&action=eth_blockNumber`; + const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; // Fetch network statistics const fetchNetworkStats = async () => { diff --git a/components/ui/RevenueGraph.tsx b/components/ui/RevenueGraph.tsx new file mode 100644 index 0000000..5093401 --- /dev/null +++ b/components/ui/RevenueGraph.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; + +const data = [ + { month: 'Jan', revenue2024: 15, revenue2023: -15 }, + { month: 'Feb', revenue2024: 5, revenue2023: -18 }, + { month: 'Mar', revenue2024: 12, revenue2023: -10 }, + { month: 'Apr', revenue2024: 25, revenue2023: -15 }, + { month: 'May', revenue2024: 15, revenue2023: -5 }, + { month: 'Jun', revenue2024: 10, revenue2023: -17 }, + { month: 'Jul', revenue2024: 7, revenue2023: -15 }, + { month: 'Aug', revenue2024: 15, revenue2023: -5 }, + { month: 'Sep', revenue2024: 10, revenue2023: -17 }, + { month: 'Oct', revenue2024: 7, revenue2023: -15 }, + { month: 'Nov', revenue2024: 15, revenue2023: -5 }, + { month: 'Dec', revenue2024: 20, revenue2023: -17 }, +]; + +export default function RevenueGraph() { + return ( + + +
+ Total Revenue +
+
+
+ 2024 +
+
+
+ 2023 +
+
+
+
+ +
+ + + + `${value}`} + /> + + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/ui/WalletCharts.tsx b/components/ui/WalletCharts.tsx new file mode 100644 index 0000000..f1e2fab --- /dev/null +++ b/components/ui/WalletCharts.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Line, LineChart, BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Area, AreaChart, PieChart, Pie, Cell } from "recharts"; + +const transactionTypeData = [ + { week: 'W1', defi: 450, nft: 320, swap: 230 }, + { week: 'W2', defi: 520, nft: 280, swap: 310 }, + { week: 'W3', defi: 710, nft: 420, swap: 380 }, + { week: 'W4', defi: 480, nft: 350, swap: 290 }, + { week: 'W5', defi: 520, nft: 390, swap: 420 }, + { week: 'W6', defi: 630, nft: 450, swap: 380 }, +]; + +const gasUsageData = [ + { name: 'Smart Contracts', usage: 45, percentage: '45%' }, + { name: 'Token Transfers', usage: 30, percentage: '30%' }, + { name: 'NFT Trading', usage: 15, percentage: '15%' }, + { name: 'Other', usage: 10, percentage: '10%' }, +]; + +const miniChartData = { + tokenHoldings: [ + { name: 'ETH', value: 60 }, + { name: 'USDT', value: 25 }, + { name: 'Other', value: 15 }, + ], + walletAge: [ + { date: '1', value: 10 }, + { date: '2', value: 15 }, + { date: '3', value: 12 }, + { date: '4', value: 18 }, + { date: '5', value: 22 }, + { date: '6', value: 20 }, + ], + transactionSuccess: [ + { name: 'Success', value: 85 }, + { name: 'Failed', value: 15 }, + ], + networkInteractions: [ + { name: 'DeFi Protocols', value: 40 }, + { name: 'DEX', value: 30 }, + { name: 'NFT Markets', value: 20 }, + { name: 'Others', value: 10 }, + ], +}; + +const COLORS = ['#F5B056', '#a855f7', '#22c55e', '#666']; + +export default function WalletCharts() { + const tooltipStyle = { + backgroundColor: '#1f2937', + border: 'none', + borderRadius: '8px', + color: '#fff' + }; + + const tooltipFormatter = (value: number, name: string) => { + return [ + `${value}%`, + `${name}`, + ]; + }; + + return ( +
+
+ {/* Transaction Types Overview */} + + +
+ Transaction Types +
+
+
+ DeFi +
+
+
+ NFT +
+
+
+ Swap +
+
+
+
+ +
+ + + + + + + + + + +
+
+
+ + {/* Gas Usage Distribution */} + + +
+ Gas Usage Distribution +
+
+ +
+ {gasUsageData.map((item, index) => ( +
+
+ {item.name} + {item.percentage} +
+
+
+
+
+ ))} +
+
+
+
+ +
+ {/* Token Distribution */} + + + Token Distribution + + +
+ + + + {miniChartData.tokenHoldings.map((entry, index) => ( + + ))} + + + + +
+
+
+ + {/* Wallet Age Activity */} + + + Wallet Age Activity + + +
+ + + + + + +
+
+
+ + {/* Transaction Success Rate */} + + +
+ Success Rate +
85%
+
+
+ +
+ + + + + + + + + +
+
+
+ + {/* Network Interactions */} + + + Network Interactions + + +
+ + + + {miniChartData.networkInteractions.map((entry, index) => ( + + ))} + + + + +
+
+
+
+
+ ); +} \ No newline at end of file From a0f644c653a8f1ec8339643d4c8ee811f7a67a8d Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Sat, 8 Mar 2025 12:08:37 +0700 Subject: [PATCH 003/107] add icons --- components/ui/NetworkStats.tsx | 96 +++++++++++++++++++--------------- components/ui/WalletCharts.tsx | 31 ++++++++--- 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/components/ui/NetworkStats.tsx b/components/ui/NetworkStats.tsx index 421402e..4bdb8ef 100644 --- a/components/ui/NetworkStats.tsx +++ b/components/ui/NetworkStats.tsx @@ -2,6 +2,7 @@ import { useState, useEffect} from 'react' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Clock, Loader2, Gauge, Calculator } from "lucide-react" import axios from 'axios'; import TransactionTable from '@/components/ui/TransactionTable'; @@ -128,49 +129,62 @@ useEffect(() => {
{/* Statistics cards */} -
- - Transactions (24h) - - -

- {stats.transactions24h.toLocaleString()} -

-
-
- - - - Pending Txns - - -

{stats.pendingTransactions.toLocaleString()}

-
-
- - - - Network Fee - - -

{stats.networkFee.toFixed(2)} Gwei

-
-
- - - - AVG Gas Fee - - -

{stats.avgGasFee.toFixed(2)} Gwei

-
-
-
- +
+ + +
+ + Transactions (24h) +
+
+ +

+ {stats.transactions24h.toLocaleString()} +

+
+
+ + + +
+ + Pending Txns +
+
+ +

{stats.pendingTransactions.toLocaleString()}

+
+
+ + + +
+ + Network Fee +
+
+ +

{stats.networkFee.toFixed(2)} Gwei

+
+
+ + + +
+ + AVG Gas Fee +
+
+ +

{stats.avgGasFee.toFixed(2)} Gwei

+
+
+
+ +
- - ); + ); } diff --git a/components/ui/WalletCharts.tsx b/components/ui/WalletCharts.tsx index f1e2fab..7a63dcd 100644 --- a/components/ui/WalletCharts.tsx +++ b/components/ui/WalletCharts.tsx @@ -2,6 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Line, LineChart, BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Area, AreaChart, PieChart, Pie, Cell } from "recharts"; +import { BarChart3, Gauge, Wallet, History, CheckCircle2, Network } from "lucide-react"; const transactionTypeData = [ { week: 'W1', defi: 450, nft: 320, swap: 230 }, @@ -69,7 +70,10 @@ export default function WalletCharts() {
- Transaction Types +
+ + Transaction Types +
@@ -113,7 +117,10 @@ export default function WalletCharts() {
- Gas Usage Distribution +
+ + Gas Usage Distribution +
@@ -145,7 +152,10 @@ export default function WalletCharts() { {/* Token Distribution */} - Token Distribution +
+ + Token Distribution +
@@ -178,7 +188,10 @@ export default function WalletCharts() { {/* Wallet Age Activity */} - Wallet Age Activity +
+ + Wallet Age Activity +
@@ -209,7 +222,10 @@ export default function WalletCharts() {
- Success Rate +
+ + Success Rate +
85%
@@ -242,7 +258,10 @@ export default function WalletCharts() { {/* Network Interactions */} - Network Interactions +
+ + Network Interactions +
From 4281e5304448f7a7caf0f7f081b6490c7244538f Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Sat, 8 Mar 2025 12:17:29 +0700 Subject: [PATCH 004/107] add link for pages.tsx in transactions --- app/transactions/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index a589682..0907ee0 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -3,6 +3,8 @@ import Link from 'next/link'; import NetworkStats from '@/components/ui/NetworkStats'; import ParticlesBackground from '@/components/ParticlesBackground'; +import RevenueGraph from '@/components/ui/RevenueGraph'; +import WalletCharts from '@/components/ui/WalletCharts'; export default function TransactionExplorer() { return ( From c18ce566b4a34ab77f29f3b666decfe2166fb902 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Sat, 8 Mar 2025 13:47:32 +0700 Subject: [PATCH 005/107] add wallet graph back to transaction page --- app/transactions/page.tsx | 4 ++++ components/ui/TransactionTable.tsx | 6 ++---- lib/types.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 lib/types.ts diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 0907ee0..028f5da 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -14,6 +14,10 @@ export default function TransactionExplorer() {
{/* Main Content */}
+
+ +
+
diff --git a/components/ui/TransactionTable.tsx b/components/ui/TransactionTable.tsx index 90bce34..21c8408 100644 --- a/components/ui/TransactionTable.tsx +++ b/components/ui/TransactionTable.tsx @@ -7,8 +7,6 @@ import Link from 'next/link' import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' import { toast } from "@/components/ui/use-toast" import { ethers } from 'ethers'; -import { formatEther } from 'ethers/lib/utils'; - interface Stats { transactions24h: number; @@ -132,8 +130,8 @@ const getRelativeTime = (timestamp: number) => { age: getRelativeTime(timestamp), from: tx.from, to: tx.to || 'Contract Creation', - amount: formatEther(tx.value) + ' ETH', - fee: formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), + amount: ethers.utils.formatEther(tx.value) + ' ETH', + fee: ethers.utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), timestamp: timestamp } }) diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..82dd2b6 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,9 @@ +export interface TransactionTableProps { + data?: { + id: string + from: string + to: string + value: string + timestamp: string + }[] +} \ No newline at end of file From 6d9d2de834206da6bfd1c1b26382be7fd507ad20 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Mon, 10 Mar 2025 17:32:29 +0700 Subject: [PATCH 006/107] rename file and optimize transaction page --- app/search/TransactionContent.tsx | 95 ++++- app/transactions/page.tsx | 9 +- components/TransactionTable.tsx | 207 ---------- .../transactions/FilteredTransactionTable.tsx | 310 +++++++++++++++ components/transactions/NetworkStats.tsx | 172 ++++++++ .../{ui => transactions}/RevenueGraph.tsx | 0 .../{ui => transactions}/TransactionTable.tsx | 78 ++-- .../{ui => transactions}/WalletCharts.tsx | 0 components/ui/NetworkStats.tsx | 191 --------- next.config.js | 23 ++ package-lock.json | 369 ++++++++++++++++++ package.json | 4 + 12 files changed, 1000 insertions(+), 458 deletions(-) delete mode 100644 components/TransactionTable.tsx create mode 100644 components/transactions/FilteredTransactionTable.tsx create mode 100644 components/transactions/NetworkStats.tsx rename components/{ui => transactions}/RevenueGraph.tsx (100%) rename components/{ui => transactions}/TransactionTable.tsx (92%) rename components/{ui => transactions}/WalletCharts.tsx (100%) delete mode 100644 components/ui/NetworkStats.tsx create mode 100644 next.config.js diff --git a/app/search/TransactionContent.tsx b/app/search/TransactionContent.tsx index 499b8a5..774f198 100644 --- a/app/search/TransactionContent.tsx +++ b/app/search/TransactionContent.tsx @@ -1,17 +1,75 @@ 'use client' +import dynamic from 'next/dynamic' +import { Suspense, useEffect } from 'react' import SearchBar from "@/components/SearchBar" import WalletInfo from "@/components/WalletInfo" -import TransactionGraph from "@/components/TransactionGraph" -import TransactionTable from "@/components/TransactionTable" -import Portfolio from "@/components/Portfolio" -import NFTGallery from "@/components/NFTGallery" +import { Card, CardContent } from "@/components/ui/card" import { useSearchParams } from "next/navigation" +import { Loader2 } from "lucide-react" +// Preload critical components +const FilteredTransactionTable = dynamic(() => import("@/components/transactions/FilteredTransactionTable"), { + loading: () => Loading transactions..., + ssr: false +}) + +// Defer loading of non-critical components +const TransactionGraph = dynamic(() => import("@/components/TransactionGraph"), { + loading: () => Loading transaction graph..., + ssr: false, +}) + +const Portfolio = dynamic(() => import("@/components/Portfolio"), { + loading: () => Loading portfolio..., + ssr: false, +}) + +const NFTGallery = dynamic(() => import("@/components/NFTGallery"), { + loading: () => Loading NFTs..., + ssr: false, +}) + +// Loading component optimized for frequent reuse +const LoadingCard = ({ children }: { children: React.ReactNode }) => ( + + + +

{children}

+
+
+) export default function Transactions() { const searchParams = useSearchParams() const address = searchParams.get("address") + + // Preload components when address is available + useEffect(() => { + if (address) { + // Preload critical components immediately + const preloadCritical = async () => { + const [FilteredTransactionTable, WalletInfo] = await Promise.all([ + import("@/components/transactions/FilteredTransactionTable"), + import("@/components/WalletInfo") + ]) + } + preloadCritical() + + // Defer loading of non-critical components + const preloadNonCritical = async () => { + const [TransactionGraph, Portfolio, NFTGallery] = await Promise.all([ + import("@/components/TransactionGraph"), + import("@/components/Portfolio"), + import("@/components/NFTGallery") + ]) + } + // Delay loading non-critical components + const timer = setTimeout(preloadNonCritical, 2000) + return () => clearTimeout(timer) + } + }, [address]) + return (
@@ -20,15 +78,32 @@ export default function Transactions() {
{address ? ( <> -
+ {/* Critical content loaded first */} + Loading transactions...}> + + + + {/* Non-critical content loaded after */} +
- - + Loading wallet info...}> + + + Loading portfolio...}> + +
- + Loading graph...}> + + +
+ + {/* Load NFTs last */} +
+ Loading NFTs...}> + +
- - ) : (
diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 028f5da..6f4fa49 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -1,10 +1,13 @@ 'use client'; import Link from 'next/link'; -import NetworkStats from '@/components/ui/NetworkStats'; +import { Metadata } from "next" +import { Suspense } from "react" +import NetworkStats from '@/components/transactions/NetworkStats'; import ParticlesBackground from '@/components/ParticlesBackground'; -import RevenueGraph from '@/components/ui/RevenueGraph'; -import WalletCharts from '@/components/ui/WalletCharts'; +import RevenueGraph from '@/components/transactions/RevenueGraph'; +import { Skeleton } from "@/components/ui/skeleton" +import WalletCharts from '@/components/transactions/WalletCharts'; export default function TransactionExplorer() { return ( diff --git a/components/TransactionTable.tsx b/components/TransactionTable.tsx deleted file mode 100644 index 16e8c9b..0000000 --- a/components/TransactionTable.tsx +++ /dev/null @@ -1,207 +0,0 @@ -"use client" - -import { useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Button } from "@/components/ui/button" -import { Loader2 } from "lucide-react" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { TransactionTableProps } from '@/lib/types' - -interface Transaction { - id: string - from: string - to: string - value: string - timestamp: string - type: "transfer" | "swap" | "inflow" | "outflow" -} - -export default function TransactionTable({ data }: TransactionTableProps) { - const searchParams = useSearchParams() - const address = searchParams.get("address") - const [transactions, setTransactions] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [page, setPage] = useState(1) - - useEffect(() => { - if (address) { - setLoading(true) - setError(null) - fetch(`/api/transactions?address=${address}&page=${page}&offset=20`) - .then((res) => res.json()) - .then((data) => { - if (data.error) { - throw new Error(data.error) - } - // Mock categorization of transactions - const categorizedData = data.map((tx: Transaction) => ({ - ...tx, - type: categorizeTransaction(tx, address), - })) - setTransactions(categorizedData) - }) - .catch((err) => { - console.error("Error fetching transactions:", err) - setError(err.message || "Failed to fetch transactions") - }) - .finally(() => setLoading(false)) - } - }, [address, page]) - - const categorizeTransaction = (tx: Transaction, userAddress: string): Transaction["type"] => { - if (tx.from === userAddress && tx.to === userAddress) return "swap" - if (tx.from === userAddress) return "outflow" - if (tx.to === userAddress) return "inflow" - return "transfer" - } - - if (loading) { - return ( - - - - - - ) - } - - if (error) { - return ( - - Error - {error} - - ) - } - - if (transactions.length === 0) { - return ( - - No transactions found. - - ) - } - - const renderTransactionTable = (transactions: Transaction[]) => ( - - - - From - To - Value - Timestamp - - - - {transactions.map((tx) => ( - - - {tx.from.slice(0, 6)}...{tx.from.slice(-4)} - - - {tx.to.slice(0, 6)}...{tx.to.slice(-4)} - - {tx.value} - {new Date(tx.timestamp).toLocaleString()} - - ))} - -
- ) - - return ( - - - Recent Transactions - - - - - - All - - - Transfer - - - Swap - - - Inflow - - - Outflow - - - {renderTransactionTable(transactions)} - - {renderTransactionTable(transactions.filter((tx) => tx.type === "transfer"))} - - - {renderTransactionTable(transactions.filter((tx) => tx.type === "swap"))} - - - {renderTransactionTable(transactions.filter((tx) => tx.type === "inflow"))} - - - {renderTransactionTable(transactions.filter((tx) => tx.type === "outflow"))} - - -
- - -
-
-
- ) -} - diff --git a/components/transactions/FilteredTransactionTable.tsx b/components/transactions/FilteredTransactionTable.tsx new file mode 100644 index 0000000..b1af3a6 --- /dev/null +++ b/components/transactions/FilteredTransactionTable.tsx @@ -0,0 +1,310 @@ +"use client" + +import { useSearchParams } from "next/navigation" +import { useEffect, useState, useCallback, useMemo, useRef, useTransition } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Button } from "@/components/ui/button" +import { Loader2 } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { TransactionTableProps } from '@/lib/types' +import { useVirtualizer } from '@tanstack/react-virtual' + +interface Transaction { + id: string + from: string + to: string + value: string + timestamp: string + type: "transfer" | "swap" | "inflow" | "outflow" +} + +// Cache for storing transaction data with LRU eviction +class LRUCache { + private cache: Map + private maxSize: number + + constructor(maxSize = 50) { + this.cache = new Map() + this.maxSize = maxSize + } + + get(key: string): { data: Transaction[], timestamp: number } | undefined { + const item = this.cache.get(key) + if (item) { + // Move to front (most recently used) + this.cache.delete(key) + this.cache.set(key, item) + } + return item + } + + set(key: string, value: { data: Transaction[], timestamp: number }): void { + if (this.cache.size >= this.maxSize) { + // Remove least recently used item + const firstKey = Array.from(this.cache.keys())[0] + if (firstKey) { + this.cache.delete(firstKey) + } + } + this.cache.set(key, value) + } + + clear(): void { + this.cache.clear() + } +} + +const transactionCache = new LRUCache(50) +const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes + +// Memoized tab options +const TAB_OPTIONS = { + all: "All", + transfer: "Transfer", + swap: "Swap", + inflow: "Inflow", + outflow: "Outflow" +} as const + +export default function FilteredTransactionTable({ data }: TransactionTableProps) { + const searchParams = useSearchParams() + const address = searchParams.get("address") + const [transactions, setTransactions] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [page, setPage] = useState(1) + const [isPending, startTransition] = useTransition() + const parentRef = useRef(null) + const abortControllerRef = useRef(null) + + // Memoize the cache key + const cacheKey = useMemo(() => `${address}-${page}`, [address, page]) + + // Virtual list setup + const rowVirtualizer = useVirtualizer({ + count: transactions.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 40, // estimated row height + overscan: 5 + }) + + const fetchTransactions = useCallback(async () => { + if (!address) return + + // Check cache first + const cached = transactionCache.get(cacheKey) + if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { + startTransition(() => { + setTransactions(cached.data) + }) + return + } + + // Cancel previous request if exists + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + + // Create new abort controller + abortControllerRef.current = new AbortController() + + setLoading(true) + setError(null) + + try { + const res = await fetch( + `/api/transactions?address=${address}&page=${page}&offset=20`, + { signal: abortControllerRef.current.signal } + ) + const data = await res.json() + + if (data.error) throw new Error(data.error) + + const categorizedData = data.map((tx: Transaction) => ({ + ...tx, + type: categorizeTransaction(tx, address), + })) + + // Update cache + transactionCache.set(cacheKey, { + data: categorizedData, + timestamp: Date.now(), + }) + + startTransition(() => { + setTransactions(categorizedData) + }) + } catch (err: any) { + if (err.name === 'AbortError') return + console.error("Error fetching transactions:", err) + setError(err.message || "Failed to fetch transactions") + } finally { + setLoading(false) + abortControllerRef.current = null + } + }, [address, page, cacheKey]) + + // Cleanup effect + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + } + }, []) + + // Debounced fetch effect + useEffect(() => { + const timer = setTimeout(fetchTransactions, 300) + return () => clearTimeout(timer) + }, [fetchTransactions]) + + const categorizeTransaction = useCallback((tx: Transaction, userAddress: string): Transaction["type"] => { + if (tx.from === userAddress && tx.to === userAddress) return "swap" + if (tx.from === userAddress) return "outflow" + if (tx.to === userAddress) return "inflow" + return "transfer" + }, []) + + // Memoize filtered transactions with type checking + const filteredTransactions = useMemo(() => ({ + all: transactions, + transfer: transactions.filter((tx) => tx.type === "transfer"), + swap: transactions.filter((tx) => tx.type === "swap"), + inflow: transactions.filter((tx) => tx.type === "inflow"), + outflow: transactions.filter((tx) => tx.type === "outflow"), + }), [transactions]) + + // Memoize table rendering function + const renderTransactionTable = useCallback((transactions: Transaction[]) => ( +
+ + + + From + To + Value + Timestamp + + + +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const tx = transactions[virtualRow.index] + return ( + + + {tx.from.slice(0, 6)}...{tx.from.slice(-4)} + + + {tx.to.slice(0, 6)}...{tx.to.slice(-4)} + + {tx.value} + {new Date(tx.timestamp).toLocaleString()} + + ) + })} +
+
+
+
+ ), [rowVirtualizer]) + + if (loading) { + return ( + + + + + + ) + } + + if (error) { + return ( + + Error + {error} + + ) + } + + if (transactions.length === 0) { + return ( + + No transactions found. + + ) + } + + return ( + + + Recent Transactions + + + + + {Object.entries(TAB_OPTIONS).map(([value, label]) => ( + + {label} + + ))} + + {Object.entries(filteredTransactions).map(([type, txs]) => ( + + {renderTransactionTable(txs)} + + ))} + +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx new file mode 100644 index 0000000..87a4bbc --- /dev/null +++ b/components/transactions/NetworkStats.tsx @@ -0,0 +1,172 @@ +'use client' + +import { useState, useEffect} from 'react' +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Clock, Loader2, Gauge, Calculator } from "lucide-react" +import axios from 'axios'; +import TransactionTable from '@/components/transactions/TransactionTable'; + +interface Stats { + transactions24h: number; + pendingTransactions: number; + networkFee: number; + avgGasFee: number; + totalTransactionAmount: number; +} + +const initialStats: Stats = { + transactions24h: 0, + pendingTransactions: 0, + networkFee: 0, + avgGasFee: 0, + totalTransactionAmount: 0, +}; + +export default function TransactionExplorer() { + const [, setIsMobile] = useState(false); + const [stats, setStats] = useState(initialStats); + const [, setTotalTransactions] = useState(0); + const [, setLoading] = useState(true); + const [, setError] = useState(null); + + const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; + const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; + + const fetchNetworkStats = async () => { + try { + const gasResponse = await fetch( + `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` + ); + const gasData = await gasResponse.json(); + + if (gasData.status === "1") { + setStats(prev => ({ + ...prev, + networkFee: parseFloat(gasData.result.SafeGasPrice), + avgGasFee: parseFloat(gasData.result.ProposeGasPrice) + })); + } + + const blockResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` + ); + const blockData = await blockResponse.json(); + const latestBlock = parseInt(blockData.result, 16); + + const blocksIn24h = Math.floor(86400 / 15); + + const txCountResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` + ); + const txCountData = await txCountResponse.json(); + const txCount = parseInt(txCountData.result, 16); + + setStats(prev => ({ + ...prev, + transactions24h: txCount * blocksIn24h, + pendingTransactions: txCount + })); + } catch (error) { + console.error('Error fetching network stats:', error); + } + }; + + const fetchTotalTransactions = async () => { + setLoading(true); + try { + const response = await axios.get(API_URL); + const totalTxCount = response.data.result; + setTotalTransactions(Number(totalTxCount)); + } catch (err) { + setError('Lỗi khi lấy dữ liệu từ API'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTotalTransactions(); + const interval = setInterval(() => { + fetchTotalTransactions(); + }, 300000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + fetchNetworkStats(); + const interval = setInterval(() => { + fetchNetworkStats(); + }, 30000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return ( +
+
+
+ + +
+ + Transactions (24h) +
+
+ +

+ {stats.transactions24h.toLocaleString()} +

+
+
+ + + +
+ + Pending Txns +
+
+ +

{stats.pendingTransactions.toLocaleString()}

+
+
+ + + +
+ + Network Fee +
+
+ +

{stats.networkFee.toFixed(2)} Gwei

+
+
+ + + +
+ + AVG Gas Fee +
+
+ +

{stats.avgGasFee.toFixed(2)} Gwei

+
+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/components/ui/RevenueGraph.tsx b/components/transactions/RevenueGraph.tsx similarity index 100% rename from components/ui/RevenueGraph.tsx rename to components/transactions/RevenueGraph.tsx diff --git a/components/ui/TransactionTable.tsx b/components/transactions/TransactionTable.tsx similarity index 92% rename from components/ui/TransactionTable.tsx rename to components/transactions/TransactionTable.tsx index 21c8408..d0dad94 100644 --- a/components/ui/TransactionTable.tsx +++ b/components/transactions/TransactionTable.tsx @@ -13,7 +13,7 @@ interface Stats { pendingTransactions: number; networkFee: number; avgGasFee: number; - totalTransactionAmount: number; // New field for total transaction amount + totalTransactionAmount: number; } // Initial state @@ -22,7 +22,7 @@ const initialStats: Stats = { pendingTransactions: 0, networkFee: 0, avgGasFee: 0, - totalTransactionAmount: 0, // Initialize to 0 + totalTransactionAmount: 0, }; export default function TransactionExplorer() { @@ -34,9 +34,8 @@ export default function TransactionExplorer() { const [isMobile, setIsMobile] = useState(false); const [isLoading, setIsLoading] = useState(false); - // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key + const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; interface MethodSignatures { @@ -78,23 +77,23 @@ export default function TransactionExplorer() { }; // Function to get relative time -const getRelativeTime = (timestamp: number) => { - const now = Date.now(); - const diff = now - timestamp * 1000; - - // Ensure diff is not negative - if (diff < 0) return "Just now"; - - const seconds = Math.floor(diff / 1000); - - if (seconds < 60) return `${seconds} secs ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} mins ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours} hrs ago`; - const days = Math.floor(hours / 24); - return `${days} days ago`; -}; + const getRelativeTime = (timestamp: number) => { + const now = Date.now(); + const diff = now - timestamp * 1000; + + // Ensure diff is not negative + if (diff < 0) return "Just now"; + + const seconds = Math.floor(diff / 1000); + + if (seconds < 60) return `${seconds} secs ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} mins ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hrs ago`; + const days = Math.floor(hours / 24); + return `${days} days ago`; + }; // Function to truncate addresses const truncateAddress = (address: string) => { @@ -148,7 +147,7 @@ const getRelativeTime = (timestamp: number) => { } finally { setIsLoading(false) } - }, [toast]) + }, [ETHERSCAN_API_KEY, API_URL]) useEffect(() => { fetchLatestTransactions() @@ -156,8 +155,6 @@ const getRelativeTime = (timestamp: number) => { return () => clearInterval(interval) }, [fetchLatestTransactions]) - - // Effect to fetch data useEffect(() => { fetchLatestTransactions(); const interval = setInterval(() => { @@ -177,7 +174,6 @@ const getRelativeTime = (timestamp: number) => { return () => window.removeEventListener('resize', handleResize); }, []); - // Utility functions (handleDownload, copyToClipboard, etc.) const copyToClipboard = async (text: string) => { try { @@ -228,24 +224,19 @@ const getRelativeTime = (timestamp: number) => { }; const formatTimestamp = (timestamp: number): string => { - // Create a date object from the timestamp - const date = new Date(timestamp * 1000); // Convert seconds to milliseconds - - // Convert to GMT+7 + const date = new Date(timestamp * 1000); const options: Intl.DateTimeFormatOptions = { - timeZone: 'Asia/Bangkok', // GMT+7 timezone + timeZone: 'Asia/Bangkok', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false // Use 24-hour format + hour12: false }; - - // Format the date - return date.toLocaleString('en-GB', options).replace(',', ''); // Remove comma for better CSV formatting -}; + return date.toLocaleString('en-GB', options).replace(',', ''); + }; const handleMethodClick = (method: string) => { setSelectedMethod(method === selectedMethod ? null : method); @@ -258,8 +249,6 @@ const getRelativeTime = (timestamp: number) => { return (
- - {/* Transaction table header */}
@@ -319,10 +308,9 @@ const getRelativeTime = (timestamp: number) => {
- -{/* Transaction table */} -
- + {/* Transaction table */} +
+
@@ -345,7 +333,7 @@ const getRelativeTime = (timestamp: number) => { ) : ( transactions.map((tx, index) => ( - +
@@ -502,8 +490,4 @@ const formatFee = (fee: string) => { if (!fee) return '0'; const value = parseFloat(fee); return value.toFixed(6); -}; - - - - \ No newline at end of file +}; \ No newline at end of file diff --git a/components/ui/WalletCharts.tsx b/components/transactions/WalletCharts.tsx similarity index 100% rename from components/ui/WalletCharts.tsx rename to components/transactions/WalletCharts.tsx diff --git a/components/ui/NetworkStats.tsx b/components/ui/NetworkStats.tsx deleted file mode 100644 index 4bdb8ef..0000000 --- a/components/ui/NetworkStats.tsx +++ /dev/null @@ -1,191 +0,0 @@ -'use client' - -import { useState, useEffect} from 'react' -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Clock, Loader2, Gauge, Calculator } from "lucide-react" -import axios from 'axios'; -import TransactionTable from '@/components/ui/TransactionTable'; - -interface Stats { - transactions24h: number; - pendingTransactions: number; - networkFee: number; - avgGasFee: number; - totalTransactionAmount: number; // New field for total transaction amount -} - -// Initial state -const initialStats: Stats = { - transactions24h: 0, - pendingTransactions: 0, - networkFee: 0, - avgGasFee: 0, - totalTransactionAmount: 0, // Initialize to 0 -}; - -export default function TransactionExplorer() { - // State variables - const [, setIsMobile] = useState(false); - const [stats, setStats] = useState(initialStats); - const [, setTotalTransactions] = useState(0); - const [, setLoading] = useState(true); - const [, setError] = useState(null); - - - // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key - const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; - - // Fetch network statistics - const fetchNetworkStats = async () => { - try { - // Get gas price statistics - const gasResponse = await fetch( - `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` - ); - const gasData = await gasResponse.json(); - - if (gasData.status === "1") { - setStats(prev => ({ - ...prev, - networkFee: parseFloat(gasData.result.SafeGasPrice), - avgGasFee: parseFloat(gasData.result.ProposeGasPrice) - })); - } - - // Get 24h transaction count (approximate) - const blockResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` - ); - const blockData = await blockResponse.json(); - const latestBlock = parseInt(blockData.result, 16); - - // Assuming ~15 second block time, calculate blocks in 24h - const blocksIn24h = Math.floor(86400 / 15); - - // Get transaction count for latest block - const txCountResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` - ); - const txCountData = await txCountResponse.json(); - const txCount = parseInt(txCountData.result, 16); - - setStats(prev => ({ - ...prev, - transactions24h: txCount * blocksIn24h, // Rough estimation - pendingTransactions: txCount // Current block's transaction count as pending - })); - } catch (error) { - console.error('Error fetching network stats:', error); - } - }; - - const fetchTotalTransactions = async () => { - setLoading(true); // Đặt loading thành true trước khi gọi API - try { - const response = await axios.get(API_URL); - const totalTxCount = response.data.result; // Giả định bạn có cách lấy số giao dịch từ API - - setTotalTransactions(Number(totalTxCount)); - } catch (err) { - setError('Lỗi khi lấy dữ liệu từ API'); - } finally { - setLoading(false); - } -}; - -useEffect(() => { - fetchTotalTransactions(); - const interval = setInterval(() => { - fetchTotalTransactions(); - }, 300000); - - return () => clearInterval(interval); -}, []); - - - useEffect(() => { - fetchNetworkStats(); - const interval = setInterval(() => { - fetchNetworkStats(); - }, 30000); // Refresh every 5 minutes - - return () => clearInterval(interval); - }, []); - - - // Effect to handle responsive design - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - - return ( -
-
- {/* Statistics cards */} -
- - -
- - Transactions (24h) -
-
- -

- {stats.transactions24h.toLocaleString()} -

-
-
- - - -
- - Pending Txns -
-
- -

{stats.pendingTransactions.toLocaleString()}

-
-
- - - -
- - Network Fee -
-
- -

{stats.networkFee.toFixed(2)} Gwei

-
-
- - - -
- - AVG Gas Fee -
-
- -

{stats.avgGasFee.toFixed(2)} Gwei

-
-
-
- - -
-
- ); -} - - - \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..68a2c10 --- /dev/null +++ b/next.config.js @@ -0,0 +1,23 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack: (config, { isServer }) => { + // Handle eccrypto native module + config.resolve.fallback = { + ...config.resolve.fallback, + "crypto": require.resolve("crypto-browserify"), + "stream": require.resolve("stream-browserify"), + "path": require.resolve("path-browserify"), + "fs": false, + } + + // Ignore native module build errors + config.resolve.alias = { + ...config.resolve.alias, + "./build/Release/ecdh": false, + } + + return config + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 68b391a..7ed577f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", + "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -84,9 +85,12 @@ "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", + "crypto-browserify": "^3.12.1", "eslint": "^9", "eslint-config-next": "15.1.6", + "path-browserify": "^1.0.1", "postcss": "^8", + "stream-browserify": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -5602,6 +5606,33 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz", + "integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz", + "integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@trezor/analytics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.3.0.tgz", @@ -8699,6 +8730,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -9084,6 +9134,129 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dev": true, + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-sign/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -9618,6 +9791,13 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -9630,6 +9810,24 @@ "node": ">=0.8" } }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -9690,6 +9888,47 @@ "uncrypto": "^0.1.3" } }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/crypto-browserify/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crypto-es": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/crypto-es/-/crypto-es-1.2.7.tgz", @@ -10196,6 +10435,17 @@ "node": ">=0.4.0" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destr": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", @@ -10231,6 +10481,25 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -14566,6 +14835,27 @@ "node": ">=8.6" } }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -15319,6 +15609,38 @@ "node": ">=6" } }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse-headers": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", @@ -15331,6 +15653,13 @@ "integrity": "sha512-8e0JIqkRbMMPlFBnF9f+92hX1s07jdkd3tqB8uHE9L+cwGGjIYjQM7QLgt0FQ5MZp6SFFYYDm/Y48pqK3ZvJOQ==", "license": "MIT" }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -15714,6 +16043,13 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/process-warning": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", @@ -15773,6 +16109,28 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -15880,6 +16238,17 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", diff --git a/package.json b/package.json index 6d272a9..4598529 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", + "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -85,9 +86,12 @@ "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", + "crypto-browserify": "^3.12.1", "eslint": "^9", "eslint-config-next": "15.1.6", + "path-browserify": "^1.0.1", "postcss": "^8", + "stream-browserify": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } From d085ea37d84f8f248df6cbed46aaa2d2b0be4e0f Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Mon, 10 Mar 2025 18:10:41 +0700 Subject: [PATCH 007/107] fix conflict --- lib/types.ts | 195 +++++++++++++++- package-lock.json | 556 ++++++++++------------------------------------ package.json | 14 +- 3 files changed, 319 insertions(+), 446 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 82dd2b6..83f12ce 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,9 +1,190 @@ +// lib/types.ts + +// Transaction related types export interface TransactionTableProps { data?: { - id: string - from: string - to: string - value: string - timestamp: string - }[] -} \ No newline at end of file + id: string; + from: string; + to: string; + value: string; + timestamp: string; + }[]; +} + +// Cryptocurrency related types +export type Coin = { + id: string; + symbol: string; + name: string; + image: string; + current_price: number; + market_cap: number; + market_cap_rank: number; + fully_diluted_valuation: number; + total_volume: number; + high_24h: number; + low_24h: number; + price_change_24h: number; + price_change_percentage_24h: number; + market_cap_change_24h: number; + market_cap_change_percentage_24h: number; + circulating_supply: number; + total_supply: number; + max_supply: number; + ath: number; + ath_change_percentage: number; + ath_date: string; + atlDance: number; + atl_change_percentage: number; + atl_date: string; + roi: { + times: number; + currency: string; + percentage: number; + } | null; + last_updated: string; + sparkline_in_7d: { + price: number[]; + }; + price_change_percentage_1h_in_currency: number; + price_change_percentage_24h_in_currency: number; + price_change_percentage_7d_in_currency: number; +}; + +export type CoinDetail = { + id: string; + symbol: string; + name: string; + description: { + en: string; + }; + image: { + thumb: string; + small: string; + large: string; + }; + market_cap_rank: number; + links: { + homepage: string[]; + blockchain_site: string[]; + official_forum_url: string[]; + chat_url: string[]; + announcement_url: string[]; + twitter_screen_name: string; + facebook_username: string; + bitcointalk_thread_identifier: number | null; + telegram_channel_identifier: string; + subreddit_url: string; + repos_url: { + github: string[]; + bitbucket: string[]; + }; + }; + market_data: { + current_price: { + usd: number; + }; + market_cap: { + usd: number; + }; + market_cap_rank: number; + fully_diluted_valuation: { + usd: number; + }; + total_volume: { + usd: number; + }; + high_24h: { + usd: number; + }; + low_24h: { + usd: number; + }; + price_change_24h: number; + price_change_percentage_24h: number; + price_change_percentage_7d: number; + price_change_percentage_14d: number; + price_change_percentage_30d: number; + price_change_percentage_60d: number; + price_change_percentage_200d: number; + price_change_percentage_1y: number; + market_cap_change_24h: number; + market_cap_change_percentage_24h: number; + price_change_24h_in_currency: { + usd: number; + }; + price_change_percentage_1h_in_currency: { + usd: number; + }; + price_change_percentage_24h_in_currency: { + usd: number; + }; + price_change_percentage_7d_in_currency: { + usd: number; + }; + price_change_percentage_14d_in_currency: { + usd: number; + }; + price_change_percentage_30d_in_currency: { + usd: number; + }; + price_change_percentage_60d_in_currency: { + usd: number; + }; + price_change_percentage_200d_in_currency: { + usd: number; + }; + price_change_percentage_1y_in_currency: { + usd: number; + }; + max_supply: number; + circulating_supply: number; + total_supply: number; + sparkline_7d: { + price: number[]; + }; + ath: { + usd: number; + }; + ath_change_percentage: { + usd: number; + }; + ath_date: { + usd: string; + }; + atl: { + usd: number; + }; + atl_change_percentage: { + usd: number; + }; + atl_date: { + usd: string; + }; + }; +}; + +export type GlobalData = { + active_cryptocurrencies: number; + upcoming_icos: number; + ongoing_icos: number; + ended_icos: number; + markets: number; + total_market_cap: { + [key: string]: number; + }; + total_volume: { + [key: string]: number; + }; + market_cap_percentage: { + [key: string]: number; + }; + market_cap_change_percentage_24h_usd: number; + updated_at: number; +}; + +export type CoinHistory = { + prices: [number, number][]; + market_caps: [number, number][]; + total_volumes: [number, number][]; +}; diff --git a/package-lock.json b/package-lock.json index 7ed577f..044d400 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,8 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", - "@tanstack/react-virtual": "^3.13.2", + + "@tanstack/react-query": "^5.67.1", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -46,7 +47,7 @@ "@web3-onboard/react": "^2.11.0", "@web3-onboard/sequence": "^2.1.1", "@web3-onboard/taho": "^2.1.1", - "@web3-onboard/trezor": "^2.4.6", + "@web3-onboard/trezor": "^2.4.7", "@web3-onboard/trust": "^2.1.2", "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", @@ -54,14 +55,16 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", + "eth-crypto": "^2.7.0", "ethers": "^5.7.2", "framer-motion": "^12.4.7", "input-otp": "^1.4.2", "loading-spinner": "^1.2.1", "lucide-react": "^0.475.0", "neo4j-driver": "^5.28.1", - "next": "^15.2.0", + "next": "^15.2.1", "nodemailer": "^6.10.0", "particles.js": "^2.0.0", "pino-pretty": "^13.0.0", @@ -75,7 +78,9 @@ "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.2.0", "recharts": "^2.15.1", + "sonner": "^2.0.1", "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2" }, "devDependencies": { @@ -85,7 +90,8 @@ "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", - "crypto-browserify": "^3.12.1", + + "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", "path-browserify": "^1.0.1", @@ -303,20 +309,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -331,70 +323,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "peer": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", @@ -423,43 +351,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "peer": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -611,20 +502,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", @@ -2983,9 +2860,9 @@ } }, "node_modules/@next/env": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.0.tgz", - "integrity": "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.1.tgz", + "integrity": "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2999,9 +2876,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0.tgz", - "integrity": "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.1.tgz", + "integrity": "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==", "cpu": [ "arm64" ], @@ -3015,9 +2892,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz", - "integrity": "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.1.tgz", + "integrity": "sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==", "cpu": [ "x64" ], @@ -3031,9 +2908,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz", - "integrity": "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.1.tgz", + "integrity": "sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==", "cpu": [ "arm64" ], @@ -3047,9 +2924,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz", - "integrity": "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.1.tgz", + "integrity": "sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==", "cpu": [ "arm64" ], @@ -3063,9 +2940,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz", - "integrity": "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.1.tgz", + "integrity": "sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==", "cpu": [ "x64" ], @@ -3079,9 +2956,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz", - "integrity": "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.1.tgz", + "integrity": "sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==", "cpu": [ "x64" ], @@ -3095,9 +2972,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz", - "integrity": "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.1.tgz", + "integrity": "sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==", "cpu": [ "arm64" ], @@ -3111,9 +2988,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz", - "integrity": "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.1.tgz", + "integrity": "sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==", "cpu": [ "x64" ], @@ -5606,31 +5483,31 @@ "tslib": "^2.8.0" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz", - "integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==", + + "node_modules/@tanstack/query-core": { + "version": "5.67.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.67.1.tgz", + "integrity": "sha512-AkFmuukVejyqVIjEQoFhLb3q+xHl7JG8G9cANWTMe3s8iKzD9j1VBSYXgCjy6vm6xM8cUCR9zP2yqWxY9pTWOA==", "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.2" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.2", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz", - "integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==", + "node_modules/@tanstack/react-query": { + "version": "5.67.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.67.1.tgz", + "integrity": "sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==", "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.67.1" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" } }, "node_modules/@trezor/analytics": { @@ -6171,6 +6048,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -6223,7 +6107,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6233,12 +6117,35 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/secp256k1": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.6.tgz", @@ -8292,14 +8199,14 @@ } }, "node_modules/@web3-onboard/trezor": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@web3-onboard/trezor/-/trezor-2.4.6.tgz", - "integrity": "sha512-cP8krxZcmD6n5mh/xp8h4+6CVLqZmLL2mJrtx64n7bEsWpS3mauhe6ipQHRpMRfhS3VyoELQy7V0r9spYAZJTg==", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@web3-onboard/trezor/-/trezor-2.4.7.tgz", + "integrity": "sha512-BO8jScb9RbZJKHHOdTsFahw2CMMQi6lAdEPEPV+uoOebMp0E89hXr+tMy4KknavFiM5vBqWefq3LgqyGlt5abA==", "license": "MIT", "dependencies": { "@ethereumjs/tx": "^3.4.0", "@ethersproject/providers": "^5.5.0", - "@trezor/connect-web": "^9.0.11", + "@trezor/connect-web": "^9.5.0", "@web3-onboard/common": "^2.4.1", "@web3-onboard/hw-common": "^2.3.2", "buffer": "^6.0.3", @@ -9134,162 +9041,7 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "dev": true, - "license": "ISC", - "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/browserify-sign/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, + "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9763,13 +9515,6 @@ "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT", - "peer": true - }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -10669,13 +10414,6 @@ "node": ">=4.0.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.109", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.109.tgz", - "integrity": "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==", - "license": "ISC", - "peer": true - }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -11034,16 +10772,6 @@ "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12764,13 +12492,6 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, - "node_modules/fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", - "license": "CC0-1.0", - "peer": true - }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", @@ -13048,16 +12769,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -15060,12 +14771,12 @@ "license": "Apache-2.0" }, "node_modules/next": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.0.tgz", - "integrity": "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.1.tgz", + "integrity": "sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==", "license": "MIT", "dependencies": { - "@next/env": "15.2.0", + "@next/env": "15.2.1", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -15080,14 +14791,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.0", - "@next/swc-darwin-x64": "15.2.0", - "@next/swc-linux-arm64-gnu": "15.2.0", - "@next/swc-linux-arm64-musl": "15.2.0", - "@next/swc-linux-x64-gnu": "15.2.0", - "@next/swc-linux-x64-musl": "15.2.0", - "@next/swc-win32-arm64-msvc": "15.2.0", - "@next/swc-win32-x64-msvc": "15.2.0", + "@next/swc-darwin-arm64": "15.2.1", + "@next/swc-darwin-x64": "15.2.1", + "@next/swc-linux-arm64-gnu": "15.2.1", + "@next/swc-linux-arm64-musl": "15.2.1", + "@next/swc-linux-x64-gnu": "15.2.1", + "@next/swc-linux-x64-musl": "15.2.1", + "@next/swc-win32-arm64-msvc": "15.2.1", + "@next/swc-win32-x64-msvc": "15.2.1", "sharp": "^0.33.5" }, "peerDependencies": { @@ -15196,13 +14907,6 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT", - "peer": true - }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -17409,6 +17113,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sonner": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.1.tgz", + "integrity": "sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -17914,6 +17628,15 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tailwindcss/node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -18274,6 +17997,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18484,37 +18208,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -20306,13 +19999,6 @@ "node": ">=0.10.32" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC", - "peer": true - }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", diff --git a/package.json b/package.json index 4598529..0714b2f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", - "@tanstack/react-virtual": "^3.13.2", + + "@tanstack/react-query": "^5.67.1", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -47,7 +48,7 @@ "@web3-onboard/react": "^2.11.0", "@web3-onboard/sequence": "^2.1.1", "@web3-onboard/taho": "^2.1.1", - "@web3-onboard/trezor": "^2.4.6", + "@web3-onboard/trezor": "^2.4.7", "@web3-onboard/trust": "^2.1.2", "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", @@ -55,14 +56,16 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", + "eth-crypto": "^2.7.0", "ethers": "^5.7.2", "framer-motion": "^12.4.7", "input-otp": "^1.4.2", "loading-spinner": "^1.2.1", "lucide-react": "^0.475.0", "neo4j-driver": "^5.28.1", - "next": "^15.2.0", + "next": "^15.2.1", "nodemailer": "^6.10.0", "particles.js": "^2.0.0", "pino-pretty": "^13.0.0", @@ -76,7 +79,9 @@ "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.2.0", "recharts": "^2.15.1", + "sonner": "^2.0.1", "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2" }, "devDependencies": { @@ -86,7 +91,8 @@ "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", - "crypto-browserify": "^3.12.1", + + "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", "path-browserify": "^1.0.1", From 58e235685fad4932cbb1d8498a6df6ed0c8a82e1 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Mon, 10 Mar 2025 18:54:41 +0700 Subject: [PATCH 008/107] refix error of npm run build --- next.config.js | 24 +- next.config.js.backup | 31 +++ package-lock.json | 511 ++++++++++++++++++++++++++++++++---------- package.json | 5 +- 4 files changed, 443 insertions(+), 128 deletions(-) create mode 100644 next.config.js.backup diff --git a/next.config.js b/next.config.js index 68a2c10..c0ee7f5 100644 --- a/next.config.js +++ b/next.config.js @@ -1,19 +1,27 @@ /** @type {import('next').NextConfig} */ const nextConfig = { webpack: (config, { isServer }) => { - // Handle eccrypto native module - config.resolve.fallback = { - ...config.resolve.fallback, - "crypto": require.resolve("crypto-browserify"), - "stream": require.resolve("stream-browserify"), - "path": require.resolve("path-browserify"), - "fs": false, + if (!isServer) { + // Client-side polyfills + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + path: require.resolve('path-browserify'), + buffer: require.resolve('buffer/'), + fs: false, + net: false, + tls: false, + http: false, + https: false, + zlib: false + } } // Ignore native module build errors config.resolve.alias = { ...config.resolve.alias, - "./build/Release/ecdh": false, + './build/Release/ecdh': false, } return config diff --git a/next.config.js.backup b/next.config.js.backup new file mode 100644 index 0000000..c0ee7f5 --- /dev/null +++ b/next.config.js.backup @@ -0,0 +1,31 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack: (config, { isServer }) => { + if (!isServer) { + // Client-side polyfills + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + path: require.resolve('path-browserify'), + buffer: require.resolve('buffer/'), + fs: false, + net: false, + tls: false, + http: false, + https: false, + zlib: false + } + } + + // Ignore native module build errors + config.resolve.alias = { + ...config.resolve.alias, + './build/Release/ecdh': false, + } + + return config + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 044d400..aba358a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,8 +34,8 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", - "@tanstack/react-query": "^5.67.1", + "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -52,9 +52,11 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", + "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "crypto-browserify": "^3.12.1", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -90,7 +92,6 @@ "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", @@ -300,7 +301,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -309,6 +309,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -323,6 +337,70 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", @@ -351,6 +429,43 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -502,6 +617,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", @@ -2658,7 +2787,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3058,7 +3186,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3072,7 +3199,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3082,7 +3208,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3106,7 +3231,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5483,7 +5607,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@tanstack/query-core": { "version": "5.67.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.67.1.tgz", @@ -5510,6 +5633,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz", + "integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz", + "integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@trezor/analytics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.3.0.tgz", @@ -6107,7 +6257,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6117,7 +6267,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -8389,7 +8539,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8417,7 +8566,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { @@ -8448,7 +8596,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -8641,7 +8788,6 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.0.0", @@ -8653,7 +8799,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "dev": true, "license": "MIT" }, "node_modules/assert": { @@ -8751,7 +8896,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base-x": { @@ -8875,7 +9019,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9012,7 +9155,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9041,7 +9183,152 @@ "safe-buffer": "^5.0.1" } }, - + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-sign/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9191,7 +9478,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9259,7 +9545,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -9284,7 +9569,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9502,7 +9786,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9515,6 +9798,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT", + "peer": true + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -9540,7 +9830,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/crc-32": { @@ -9559,7 +9848,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -9570,7 +9858,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "dev": true, "license": "MIT" }, "node_modules/create-hash": { @@ -9613,7 +9900,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9637,7 +9923,6 @@ "version": "3.12.1", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "dev": true, "license": "MIT", "dependencies": { "browserify-cipher": "^1.0.1", @@ -9664,7 +9949,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -9684,7 +9968,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -10184,7 +10467,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -10223,14 +10505,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -10242,7 +10522,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "dev": true, "license": "MIT" }, "node_modules/dijkstrajs": { @@ -10255,7 +10534,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -10331,7 +10609,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/eccrypto": { @@ -10414,6 +10691,13 @@ "node": ">=4.0.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.114", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", + "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "license": "ISC", + "peer": true + }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10467,7 +10751,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/encode-utf8": { @@ -10772,6 +11055,16 @@ "@esbuild/win32-x64": "0.19.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12492,11 +12785,17 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -12525,7 +12824,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -12660,7 +12958,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -12718,7 +13015,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12769,6 +13065,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -12859,7 +13165,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -12880,7 +13185,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -12893,7 +13197,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -12903,7 +13206,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -13485,7 +13787,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13537,7 +13838,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -13588,7 +13888,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13647,7 +13946,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13699,7 +13997,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13888,7 +14185,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -13937,7 +14233,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -14030,7 +14325,6 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -14293,7 +14587,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -14306,7 +14599,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lit": { @@ -14526,7 +14818,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -14536,7 +14827,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -14550,7 +14840,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.0.0", @@ -14564,7 +14853,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "dev": true, "license": "MIT" }, "node_modules/mime-db": { @@ -14643,7 +14931,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -14703,7 +14990,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -14907,6 +15193,13 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT", + "peer": true + }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -14958,7 +15251,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15297,7 +15589,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -15317,7 +15608,6 @@ "version": "5.1.7", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "dev": true, "license": "ISC", "dependencies": { "asn1.js": "^4.10.1", @@ -15335,7 +15625,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -15377,7 +15666,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15387,14 +15675,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -15445,7 +15731,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15544,7 +15829,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15572,7 +15856,6 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15601,7 +15884,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -15619,7 +15901,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -15639,7 +15920,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15675,7 +15955,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15701,7 +15980,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15715,7 +15993,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/preact": { @@ -15751,7 +16028,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/process-warning": { @@ -15817,7 +16093,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -15832,7 +16107,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "dev": true, "license": "MIT" }, "node_modules/pump": { @@ -15904,7 +16178,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -15946,7 +16219,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, "license": "MIT", "dependencies": { "randombytes": "^2.0.5", @@ -16229,7 +16501,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -16253,7 +16524,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -16378,7 +16648,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -16419,7 +16688,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -16609,7 +16877,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -16908,7 +17175,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -16921,7 +17187,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17007,7 +17272,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -17209,7 +17473,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -17228,7 +17491,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17243,7 +17505,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17253,14 +17514,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17386,7 +17645,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -17403,7 +17661,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17416,7 +17673,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17493,7 +17749,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -17538,7 +17793,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17594,7 +17848,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -17641,7 +17894,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -17658,7 +17910,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -17704,7 +17955,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -17714,7 +17964,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -17815,7 +18064,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17847,7 +18095,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/ts-mixer": { @@ -17997,7 +18244,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18208,6 +18454,37 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -19661,7 +19938,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -19789,7 +20065,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -19808,7 +20083,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -19826,7 +20100,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19836,14 +20109,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19858,7 +20129,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19871,7 +20141,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -19999,11 +20268,17 @@ "node": ">=0.10.32" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "peer": true + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 0714b2f..ed9f04e 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", - "@tanstack/react-query": "^5.67.1", + "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -53,9 +53,11 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", + "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "crypto-browserify": "^3.12.1", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -91,7 +93,6 @@ "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", From 16cd0b13d175234ef4b5a7fc646e6af2ee0dd042 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Mon, 10 Mar 2025 19:55:02 +0700 Subject: [PATCH 009/107] readd transaction table into transation page --- app/api/pending/route.ts | 31 ++++++++ app/transactions/page.tsx | 1 + components/transactions/NetworkStats.tsx | 33 ++++---- components/transactions/TransactionTable.tsx | 81 +++++++++++--------- 4 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 app/api/pending/route.ts diff --git a/app/api/pending/route.ts b/app/api/pending/route.ts new file mode 100644 index 0000000..cf3d675 --- /dev/null +++ b/app/api/pending/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server" + +const ETHERSCAN_API_URL = "https://api.etherscan.io/api" + +export async function GET() { + try { + const response = await fetch( + `${ETHERSCAN_API_URL}?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=pending&apikey=${process.env.ETHERSCAN_API_KEY}` + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + if (data.status !== "1" && !data.result) { + throw new Error(data.message || "Etherscan API returned an error") + } + + const pendingTxCount = parseInt(data.result, 16) + + return NextResponse.json({ pendingTransactions: pendingTxCount }) + } catch (error) { + console.error("Error fetching pending transactions:", error) + return NextResponse.json( + { error: error instanceof Error ? error.message : "An unknown error occurred" }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 6f4fa49..b24a7a0 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -8,6 +8,7 @@ import ParticlesBackground from '@/components/ParticlesBackground'; import RevenueGraph from '@/components/transactions/RevenueGraph'; import { Skeleton } from "@/components/ui/skeleton" import WalletCharts from '@/components/transactions/WalletCharts'; +import TransactionTable from '@/components/transactions/TransactionTable'; export default function TransactionExplorer() { return ( diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx index 87a4bbc..8aa8562 100644 --- a/components/transactions/NetworkStats.tsx +++ b/components/transactions/NetworkStats.tsx @@ -26,17 +26,16 @@ export default function TransactionExplorer() { const [, setIsMobile] = useState(false); const [stats, setStats] = useState(initialStats); const [, setTotalTransactions] = useState(0); - const [, setLoading] = useState(true); - const [, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; const fetchNetworkStats = async () => { try { - const gasResponse = await fetch( - `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` - ); + // Fetch gas prices + const gasResponse = await fetch('/api/etherscan?module=gastracker&action=gasoracle'); const gasData = await gasResponse.json(); if (gasData.status === "1") { @@ -47,27 +46,34 @@ export default function TransactionExplorer() { })); } - const blockResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` - ); + // Fetch latest block number + const blockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber'); const blockData = await blockResponse.json(); const latestBlock = parseInt(blockData.result, 16); - const blocksIn24h = Math.floor(86400 / 15); + const blocksIn24h = Math.floor(86400 / 15); // Approximate blocks in 24h + // Fetch transaction count const txCountResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` + `/api/etherscan?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}` ); const txCountData = await txCountResponse.json(); const txCount = parseInt(txCountData.result, 16); + // Fetch pending transactions + const pendingResponse = await fetch('/api/pending'); + const pendingData = await pendingResponse.json(); + setStats(prev => ({ ...prev, transactions24h: txCount * blocksIn24h, - pendingTransactions: txCount + pendingTransactions: pendingData.pendingTransactions || 0 })); } catch (error) { console.error('Error fetching network stats:', error); + setError('Failed to fetch network stats'); + } finally { + setLoading(false); } }; @@ -95,10 +101,7 @@ export default function TransactionExplorer() { useEffect(() => { fetchNetworkStats(); - const interval = setInterval(() => { - fetchNetworkStats(); - }, 30000); - + const interval = setInterval(fetchNetworkStats, 30000); // Update every 30 seconds return () => clearInterval(interval); }, []); diff --git a/components/transactions/TransactionTable.tsx b/components/transactions/TransactionTable.tsx index d0dad94..988979c 100644 --- a/components/transactions/TransactionTable.tsx +++ b/components/transactions/TransactionTable.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button" import Link from 'next/link' import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' import { toast } from "@/components/ui/use-toast" -import { ethers } from 'ethers'; +import { utils } from 'ethers' interface Stats { transactions24h: number; @@ -35,8 +35,7 @@ export default function TransactionExplorer() { const [isLoading, setIsLoading] = useState(false); // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; - const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; + const ETHERSCAN_API_KEY = '6U137E3DGFMCCBQA8E3CAR1P1UW7EV8A6S'; interface MethodSignatures { [key: string]: string; @@ -102,26 +101,43 @@ export default function TransactionExplorer() { // Fetch latest blocks and their transactions const fetchLatestTransactions = useCallback(async () => { - if (!ETHERSCAN_API_KEY) { - console.error('Etherscan API key is not set') - return - } - try { - setIsLoading(true) - const latestBlockResponse = await fetch(API_URL) - const latestBlockData = await latestBlockResponse.json() - const latestBlock = parseInt(latestBlockData.result, 16) - + setIsLoading(true); + + // First, get the latest block number + const blockNumberResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` + ); + + if (!blockNumberResponse.ok) { + throw new Error('Failed to fetch latest block number'); + } + + const blockNumberData = await blockNumberResponse.json(); + if (blockNumberData.error) { + throw new Error(blockNumberData.error.message); + } + + const latestBlock = parseInt(blockNumberData.result, 16); + + // Then, get the transactions from the latest block const response = await fetch( `https://api.etherscan.io/api?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true&apikey=${ETHERSCAN_API_KEY}` - ) - const data = await response.json() + ); + + if (!response.ok) { + throw new Error('Failed to fetch block transactions'); + } + + const data = await response.json(); + if (data.error) { + throw new Error(data.error.message); + } if (data.result && data.result.transactions) { const formattedTransactions = await Promise.all( data.result.transactions.slice(0, 50).map(async (tx: any) => { - const timestamp = parseInt(data.result.timestamp, 16) + const timestamp = parseInt(data.result.timestamp, 16); return { hash: tx.hash, method: getTransactionMethod(tx.input), @@ -129,40 +145,31 @@ export default function TransactionExplorer() { age: getRelativeTime(timestamp), from: tx.from, to: tx.to || 'Contract Creation', - amount: ethers.utils.formatEther(tx.value) + ' ETH', - fee: ethers.utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), + amount: utils.formatEther(tx.value) + ' ETH', + fee: utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), timestamp: timestamp - } + }; }) - ) - setTransactions(formattedTransactions) + ); + setTransactions(formattedTransactions); } } catch (error) { - console.error('Error fetching transactions:', error) + console.error('Error fetching transactions:', error); toast({ title: "Error fetching transactions", - description: "Failed to fetch latest transactions.", + description: error instanceof Error ? error.message : "Failed to fetch latest transactions.", variant: "destructive", - }) + }); } finally { - setIsLoading(false) + setIsLoading(false); } - }, [ETHERSCAN_API_KEY, API_URL]) - - useEffect(() => { - fetchLatestTransactions() - const interval = setInterval(fetchLatestTransactions, 150000) // Refresh every 2.5 minutes - return () => clearInterval(interval) - }, [fetchLatestTransactions]) + }, [ETHERSCAN_API_KEY]); useEffect(() => { fetchLatestTransactions(); - const interval = setInterval(() => { - fetchLatestTransactions(); - }, 150000); // Refresh every 2.5 minutes - + const interval = setInterval(fetchLatestTransactions, 15000); // Refresh every 15 seconds return () => clearInterval(interval); - }, [currentPage]); //Refresh every page changes + }, [fetchLatestTransactions, currentPage]); // Effect to handle responsive design useEffect(() => { From a3b9b80f0bc3858aaa81a61183bac3d0e5360fbd Mon Sep 17 00:00:00 2001 From: nguyen-trg <23133049@student.hcmute.edu.vn> Date: Mon, 10 Mar 2025 20:25:18 +0700 Subject: [PATCH 010/107] aghhh --- app/pricetable/page.tsx | 4 +- components/{ => table}/ClientContent.tsx | 4 +- components/{ => table}/CoinCard.tsx | 0 components/{ => table}/CoinDetaiModal.tsx | 4 +- components/{ => table}/CoinTable.tsx | 2 +- components/{ => table}/DashboardTable.tsx | 0 components/{ => table}/TopMoversSection.tsx | 0 package-lock.json | 359 ++++++++++++++------ package.json | 1 + 9 files changed, 271 insertions(+), 103 deletions(-) rename components/{ => table}/ClientContent.tsx (97%) rename components/{ => table}/CoinCard.tsx (100%) rename components/{ => table}/CoinDetaiModal.tsx (99%) rename components/{ => table}/CoinTable.tsx (99%) rename components/{ => table}/DashboardTable.tsx (100%) rename components/{ => table}/TopMoversSection.tsx (100%) diff --git a/app/pricetable/page.tsx b/app/pricetable/page.tsx index 6f2d816..267e9c5 100644 --- a/app/pricetable/page.tsx +++ b/app/pricetable/page.tsx @@ -2,8 +2,8 @@ import ParticlesBackground from "@/components/ParticlesBackground"; import HeroSection from "@/components/HeroSection"; -import TopMoversSection from "@/components/TopMoversSection"; -import CoinTable from "@/components/CoinTable"; +import TopMoversSection from "@/components/table/TopMoversSection"; +import CoinTable from "@/components/table/CoinTable"; // ----------------- Main Page Component ----------------- const Page = () => { return ( diff --git a/components/ClientContent.tsx b/components/table/ClientContent.tsx similarity index 97% rename from components/ClientContent.tsx rename to components/table/ClientContent.tsx index 44bd465..ced8722 100644 --- a/components/ClientContent.tsx +++ b/components/table/ClientContent.tsx @@ -4,8 +4,8 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoins } from "@/lib/api/coinApi"; import HeroSection from "@/components/HeroSection"; -import CoinTable from "@/components/CoinTable"; -import CoinCard from "@/components/CoinCard"; +import CoinTable from "@/components/table/CoinTable"; +import CoinCard from "./CoinCard"; import Loader from "@/components/Loader"; export const ClientContent = () => { diff --git a/components/CoinCard.tsx b/components/table/CoinCard.tsx similarity index 100% rename from components/CoinCard.tsx rename to components/table/CoinCard.tsx diff --git a/components/CoinDetaiModal.tsx b/components/table/CoinDetaiModal.tsx similarity index 99% rename from components/CoinDetaiModal.tsx rename to components/table/CoinDetaiModal.tsx index a31d926..f56f127 100644 --- a/components/CoinDetaiModal.tsx +++ b/components/table/CoinDetaiModal.tsx @@ -20,8 +20,8 @@ import { formatNumber } from "@/lib/format"; import { formatPercentage } from "@/lib/format"; import { getColorForPercentChange } from "@/lib/format"; import { useState } from "react"; -import { Button } from "./ui/button"; -import { Separator } from "./ui/separator"; +import { Button } from "../ui/button"; +import { Separator } from "../ui/separator"; interface CoinDetailModalProps { coinId: string | null; diff --git a/components/CoinTable.tsx b/components/table/CoinTable.tsx similarity index 99% rename from components/CoinTable.tsx rename to components/table/CoinTable.tsx index 65d2bcb..294f9c3 100644 --- a/components/CoinTable.tsx +++ b/components/table/CoinTable.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; // import { HeaderTable } from '@/components/HeaderTable'; -import Loader from "./Loader"; +import Loader from "../Loader"; import { ChevronUp, ChevronDown, diff --git a/components/DashboardTable.tsx b/components/table/DashboardTable.tsx similarity index 100% rename from components/DashboardTable.tsx rename to components/table/DashboardTable.tsx diff --git a/components/TopMoversSection.tsx b/components/table/TopMoversSection.tsx similarity index 100% rename from components/TopMoversSection.tsx rename to components/table/TopMoversSection.tsx diff --git a/package-lock.json b/package-lock.json index 060a6fa..a33bf56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "dotenv": "^16.4.7", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -296,7 +297,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -305,6 +305,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -319,6 +333,70 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", @@ -347,6 +425,43 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -498,6 +613,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", @@ -2654,7 +2783,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3054,7 +3182,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3068,7 +3195,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3078,7 +3204,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3102,7 +3227,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -6102,7 +6226,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6112,7 +6236,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -8384,7 +8508,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8412,7 +8535,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { @@ -8443,7 +8565,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -8727,7 +8848,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base-x": { @@ -8851,7 +8971,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8988,7 +9107,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9017,6 +9135,39 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9166,7 +9317,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9234,7 +9384,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -9259,7 +9408,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9477,7 +9625,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9490,6 +9637,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT", + "peer": true + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -9563,7 +9717,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9593,7 +9746,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -10121,7 +10273,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/dijkstrajs": { @@ -10134,7 +10285,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -10165,6 +10315,18 @@ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -10210,7 +10372,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/eccrypto": { @@ -10293,6 +10454,13 @@ "node": ">=4.0.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.114", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", + "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "license": "ISC", + "peer": true + }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10346,7 +10514,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/encode-utf8": { @@ -10651,6 +10818,16 @@ "@esbuild/win32-x64": "0.19.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12371,11 +12548,17 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -12404,7 +12587,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -12539,7 +12721,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -12597,7 +12778,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12648,6 +12828,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -12738,7 +12928,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -12759,7 +12948,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -12772,7 +12960,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -12782,7 +12969,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -13364,7 +13550,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13416,7 +13601,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -13467,7 +13651,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13526,7 +13709,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13578,7 +13760,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13767,7 +13948,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -13816,7 +13996,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -13909,7 +14088,6 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -14172,7 +14350,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -14185,7 +14362,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lit": { @@ -14405,7 +14581,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -14415,7 +14590,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -14501,7 +14675,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -14561,7 +14734,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -14765,6 +14937,13 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT", + "peer": true + }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -14816,7 +14995,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15155,7 +15333,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -15196,7 +15373,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15206,14 +15382,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -15264,7 +15438,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15363,7 +15536,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15391,7 +15563,6 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15420,7 +15591,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -15438,7 +15608,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -15458,7 +15627,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15494,7 +15662,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -15520,7 +15687,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15534,7 +15700,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/preact": { @@ -15694,7 +15859,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -16008,7 +16172,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -16032,7 +16195,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -16157,7 +16319,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -16198,7 +16359,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -16388,7 +16548,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -16687,7 +16846,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -16700,7 +16858,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16786,7 +16943,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -16988,7 +17144,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -17007,7 +17162,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17022,7 +17176,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17032,14 +17185,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17165,7 +17316,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -17182,7 +17332,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17195,7 +17344,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17272,7 +17420,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -17317,7 +17464,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17373,7 +17519,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -17420,7 +17565,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -17437,7 +17581,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -17483,7 +17626,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -17493,7 +17635,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -17594,7 +17735,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17626,7 +17766,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/ts-mixer": { @@ -17776,7 +17915,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17987,6 +18125,37 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -19440,7 +19609,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -19568,7 +19736,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -19587,7 +19754,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -19605,7 +19771,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19615,14 +19780,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19637,7 +19800,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19650,7 +19812,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -19778,11 +19939,17 @@ "node": ">=0.10.32" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "peer": true + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 5b85e27..93037e3 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "dotenv": "^16.4.7", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", From 4293a3ef7163cb6be294ddb7f9c33e3c22aba57a Mon Sep 17 00:00:00 2001 From: Minh Duy - Mordred <95609626+TTMordred@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:30:01 +0700 Subject: [PATCH 011/107] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51c98f4..df6990d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # CryptoPath - Path Your Crypto Future **COS30049 - Computing Technology Innovation Project** -## Installation Guide - duy deo trai +## Installation Guide ### Prerequisites - Node.js 18.0 or higher From bf8f552353def6c04e752634b3b41621339add47 Mon Sep 17 00:00:00 2001 From: nguyen-trg <23133049@student.hcmute.edu.vn> Date: Tue, 11 Mar 2025 08:25:31 +0700 Subject: [PATCH 012/107] fix --- app/globals.css | 2 +- app/login/page.tsx | 39 ++------------------------------------- components/Header.tsx | 12 ++++++++++++ 3 files changed, 15 insertions(+), 38 deletions(-) diff --git a/app/globals.css b/app/globals.css index 9caeb36..b4bfd71 100644 --- a/app/globals.css +++ b/app/globals.css @@ -63,7 +63,7 @@ } .cp-button--primary { - @apply bg-[#F5B056] text-black hover:bg-[#ff6500]; + @apply bg-[#F5B056] text-black hover:bg-gray-200; } .cp-button--secondary { diff --git a/app/login/page.tsx b/app/login/page.tsx index 9970630..8948fe7 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -356,44 +356,9 @@ function LoginPageContent() {
Or continue with
-
+
{/* Social login buttons (icons only) */} - - + +
)}
@@ -290,6 +296,12 @@ const Header = () => { > Logout +
) : ( From 51b56ca03eb88c0a6c513bf2682d5d205b01cd1f Mon Sep 17 00:00:00 2001 From: nguyen-trg <23133049@student.hcmute.edu.vn> Date: Tue, 11 Mar 2025 12:30:26 +0700 Subject: [PATCH 013/107] =?UTF-8?q?s=E1=BB=ADa=20nh=C6=B0=20e=20duy=20s?= =?UTF-8?q?=E1=BB=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/table/CoinCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/table/CoinCard.tsx b/components/table/CoinCard.tsx index 79f8863..216b90f 100644 --- a/components/table/CoinCard.tsx +++ b/components/table/CoinCard.tsx @@ -4,6 +4,7 @@ import { TrendingUp, TrendingDown } from "lucide-react"; import { formatCurrency, formatPercentage } from "@/lib/format"; import CoinDetailModal from "./CoinDetaiModal"; import { useState } from "react"; +import React from 'react'; interface CoinCardProps { coin: { id: string; @@ -107,4 +108,4 @@ const CoinCard: React.FC = ({ coin, onCardClick }) => { ); }; -export default CoinCard; +export default CoinCard; \ No newline at end of file From 0a2b79c08c5fe26ef72ad81ac294af40c3854825 Mon Sep 17 00:00:00 2001 From: nguyen-trg <23133049@student.hcmute.edu.vn> Date: Tue, 11 Mar 2025 12:54:36 +0700 Subject: [PATCH 014/107] =?UTF-8?q?commit=20l=E1=BA=A1i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 346 +++++++++++++--------------------------------- 1 file changed, 96 insertions(+), 250 deletions(-) diff --git a/package-lock.json b/package-lock.json index a33bf56..ea78a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -297,6 +297,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -305,20 +306,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -333,70 +320,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "peer": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", @@ -425,43 +348,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "peer": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -613,20 +499,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", @@ -2783,6 +2655,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3182,6 +3055,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3195,6 +3069,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3204,6 +3079,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3227,6 +3103,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -6226,7 +6103,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6236,7 +6113,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -8508,6 +8385,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8535,6 +8413,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { @@ -8565,6 +8444,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -8848,6 +8728,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base-x": { @@ -8971,6 +8852,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9107,6 +8989,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9135,39 +9018,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9317,6 +9167,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9384,6 +9235,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -9408,6 +9260,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9625,6 +9478,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9637,13 +9491,6 @@ "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT", - "peer": true - }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -9717,6 +9564,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9746,6 +9594,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -10273,6 +10122,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/dijkstrajs": { @@ -10285,6 +10135,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -10372,6 +10223,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/eccrypto": { @@ -10454,13 +10306,6 @@ "node": ">=4.0.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.114", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", - "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", - "license": "ISC", - "peer": true - }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10514,6 +10359,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/encode-utf8": { @@ -10818,16 +10664,6 @@ "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12548,17 +12384,11 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, - "node_modules/fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", - "license": "CC0-1.0", - "peer": true - }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -12587,6 +12417,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -12721,6 +12552,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -12778,6 +12610,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12828,16 +12661,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -12928,6 +12751,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -12948,6 +12772,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -12960,6 +12785,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -12969,6 +12795,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -13550,6 +13377,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13601,6 +13429,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -13651,6 +13480,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13709,6 +13539,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13760,6 +13591,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13948,6 +13780,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -13996,6 +13829,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -14088,6 +13922,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -14350,6 +14185,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -14362,6 +14198,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/lit": { @@ -14581,6 +14418,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -14590,6 +14428,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -14675,6 +14514,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -14734,6 +14574,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -14937,13 +14778,6 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT", - "peer": true - }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -14995,6 +14829,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15333,6 +15168,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -15373,6 +15209,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15382,12 +15219,14 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -15438,6 +15277,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15536,6 +15376,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15563,6 +15404,7 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -15591,6 +15433,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -15608,6 +15451,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -15627,6 +15471,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -15662,6 +15507,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -15687,6 +15533,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -15700,6 +15547,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/preact": { @@ -15859,6 +15707,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -16172,6 +16021,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -16195,6 +16045,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -16319,6 +16170,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -16359,6 +16211,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -16548,6 +16401,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -16846,6 +16700,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -16858,6 +16713,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16943,6 +16799,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -17144,6 +17001,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -17162,6 +17020,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17176,6 +17035,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17185,12 +17045,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17316,6 +17178,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -17332,6 +17195,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17344,6 +17208,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17420,6 +17285,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -17464,6 +17330,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17519,6 +17386,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -17565,6 +17433,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -17581,6 +17450,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -17626,6 +17496,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -17635,6 +17506,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -17735,6 +17607,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17766,6 +17639,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/ts-mixer": { @@ -17915,6 +17789,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18125,37 +18000,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -19609,6 +19453,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -19736,6 +19581,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -19754,6 +19600,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -19771,6 +19618,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19780,12 +19628,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19800,6 +19650,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19812,6 +19663,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -19939,17 +19791,11 @@ "node": ">=0.10.32" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC", - "peer": true - }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" From 4e1050de71d2a96b15c6a0f8b82a19f8fec3a743 Mon Sep 17 00:00:00 2001 From: nguyen-trg <23133049@student.hcmute.edu.vn> Date: Tue, 11 Mar 2025 12:58:39 +0700 Subject: [PATCH 015/107] =?UTF-8?q?commit=20th=C3=AAm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pricetable/page.tsx | 2 +- components/table/ClientContent.tsx | 2 +- components/{ => table}/HeroSection.tsx | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename components/{ => table}/HeroSection.tsx (100%) diff --git a/app/pricetable/page.tsx b/app/pricetable/page.tsx index 267e9c5..f5cd74e 100644 --- a/app/pricetable/page.tsx +++ b/app/pricetable/page.tsx @@ -1,7 +1,7 @@ "use client"; import ParticlesBackground from "@/components/ParticlesBackground"; -import HeroSection from "@/components/HeroSection"; +import HeroSection from "@/components/table/HeroSection"; import TopMoversSection from "@/components/table/TopMoversSection"; import CoinTable from "@/components/table/CoinTable"; // ----------------- Main Page Component ----------------- diff --git a/components/table/ClientContent.tsx b/components/table/ClientContent.tsx index ced8722..c194ad7 100644 --- a/components/table/ClientContent.tsx +++ b/components/table/ClientContent.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoins } from "@/lib/api/coinApi"; -import HeroSection from "@/components/HeroSection"; +import HeroSection from "@/components/table/HeroSection"; import CoinTable from "@/components/table/CoinTable"; import CoinCard from "./CoinCard"; import Loader from "@/components/Loader"; diff --git a/components/HeroSection.tsx b/components/table/HeroSection.tsx similarity index 100% rename from components/HeroSection.tsx rename to components/table/HeroSection.tsx From c804722fa7fce8119cb952f146ec3136a9241be2 Mon Sep 17 00:00:00 2001 From: nguyen-trg <23133049@student.hcmute.edu.vn> Date: Tue, 11 Mar 2025 14:11:41 +0700 Subject: [PATCH 016/107] hmmm --- components/table/ClientContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/table/ClientContent.tsx b/components/table/ClientContent.tsx index c194ad7..ce4e783 100644 --- a/components/table/ClientContent.tsx +++ b/components/table/ClientContent.tsx @@ -3,8 +3,8 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoins } from "@/lib/api/coinApi"; -import HeroSection from "@/components/table/HeroSection"; -import CoinTable from "@/components/table/CoinTable"; +import HeroSection from "./HeroSection"; +import CoinTable from "./CoinTable"; import CoinCard from "./CoinCard"; import Loader from "@/components/Loader"; From 06b6a71976289c5a6fd45099bd1d23755a36ddc4 Mon Sep 17 00:00:00 2001 From: Woft257 Date: Tue, 11 Mar 2025 19:25:44 +0700 Subject: [PATCH 017/107] Remove background gradient from NFTCard component --- components/NFT/NFTCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/NFT/NFTCard.tsx b/components/NFT/NFTCard.tsx index 2e00b80..616d4b3 100644 --- a/components/NFT/NFTCard.tsx +++ b/components/NFT/NFTCard.tsx @@ -123,7 +123,6 @@ export default function NFTCard({ nft, mode, onAction, processing }: NFTCardProp quality={85} priority /> -
{/* Phần thông tin */} From 3f48f12d0b3d5167fe0902d233b9f90ca1858029 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Tue, 11 Mar 2025 19:45:51 +0700 Subject: [PATCH 018/107] readded transaction table --- app/search/TransactionContent.tsx | 4 +- app/transactions/page.tsx | 2 +- .../transactions/FilteredTransactionTable.tsx | 310 ------------------ components/transactions/NetworkStats.tsx | 4 +- .../transactions/NetworkTransactionTable.tsx | 282 ++++++++++++++++ ...ctionTable.tsx => TransactionExplorer.tsx} | 0 components/ui/TransactionTable.tsx | 205 ++++++++++++ 7 files changed, 492 insertions(+), 315 deletions(-) delete mode 100644 components/transactions/FilteredTransactionTable.tsx create mode 100644 components/transactions/NetworkTransactionTable.tsx rename components/transactions/{TransactionTable.tsx => TransactionExplorer.tsx} (100%) create mode 100644 components/ui/TransactionTable.tsx diff --git a/app/search/TransactionContent.tsx b/app/search/TransactionContent.tsx index 774f198..d47a0c2 100644 --- a/app/search/TransactionContent.tsx +++ b/app/search/TransactionContent.tsx @@ -9,7 +9,7 @@ import { useSearchParams } from "next/navigation" import { Loader2 } from "lucide-react" // Preload critical components -const FilteredTransactionTable = dynamic(() => import("@/components/transactions/FilteredTransactionTable"), { +const FilteredTransactionTable = dynamic(() => import("@/components/ui/TransactionTable"), { loading: () => Loading transactions..., ssr: false }) @@ -50,7 +50,7 @@ export default function Transactions() { // Preload critical components immediately const preloadCritical = async () => { const [FilteredTransactionTable, WalletInfo] = await Promise.all([ - import("@/components/transactions/FilteredTransactionTable"), + import("@/components/ui/TransactionTable"), import("@/components/WalletInfo") ]) } diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index b24a7a0..77831bd 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -8,7 +8,7 @@ import ParticlesBackground from '@/components/ParticlesBackground'; import RevenueGraph from '@/components/transactions/RevenueGraph'; import { Skeleton } from "@/components/ui/skeleton" import WalletCharts from '@/components/transactions/WalletCharts'; -import TransactionTable from '@/components/transactions/TransactionTable'; +import TransactionTable from '@/components/transactions/NetworkTransactionTable'; export default function TransactionExplorer() { return ( diff --git a/components/transactions/FilteredTransactionTable.tsx b/components/transactions/FilteredTransactionTable.tsx deleted file mode 100644 index b1af3a6..0000000 --- a/components/transactions/FilteredTransactionTable.tsx +++ /dev/null @@ -1,310 +0,0 @@ -"use client" - -import { useSearchParams } from "next/navigation" -import { useEffect, useState, useCallback, useMemo, useRef, useTransition } from "react" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Button } from "@/components/ui/button" -import { Loader2 } from "lucide-react" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { TransactionTableProps } from '@/lib/types' -import { useVirtualizer } from '@tanstack/react-virtual' - -interface Transaction { - id: string - from: string - to: string - value: string - timestamp: string - type: "transfer" | "swap" | "inflow" | "outflow" -} - -// Cache for storing transaction data with LRU eviction -class LRUCache { - private cache: Map - private maxSize: number - - constructor(maxSize = 50) { - this.cache = new Map() - this.maxSize = maxSize - } - - get(key: string): { data: Transaction[], timestamp: number } | undefined { - const item = this.cache.get(key) - if (item) { - // Move to front (most recently used) - this.cache.delete(key) - this.cache.set(key, item) - } - return item - } - - set(key: string, value: { data: Transaction[], timestamp: number }): void { - if (this.cache.size >= this.maxSize) { - // Remove least recently used item - const firstKey = Array.from(this.cache.keys())[0] - if (firstKey) { - this.cache.delete(firstKey) - } - } - this.cache.set(key, value) - } - - clear(): void { - this.cache.clear() - } -} - -const transactionCache = new LRUCache(50) -const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes - -// Memoized tab options -const TAB_OPTIONS = { - all: "All", - transfer: "Transfer", - swap: "Swap", - inflow: "Inflow", - outflow: "Outflow" -} as const - -export default function FilteredTransactionTable({ data }: TransactionTableProps) { - const searchParams = useSearchParams() - const address = searchParams.get("address") - const [transactions, setTransactions] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [page, setPage] = useState(1) - const [isPending, startTransition] = useTransition() - const parentRef = useRef(null) - const abortControllerRef = useRef(null) - - // Memoize the cache key - const cacheKey = useMemo(() => `${address}-${page}`, [address, page]) - - // Virtual list setup - const rowVirtualizer = useVirtualizer({ - count: transactions.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 40, // estimated row height - overscan: 5 - }) - - const fetchTransactions = useCallback(async () => { - if (!address) return - - // Check cache first - const cached = transactionCache.get(cacheKey) - if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { - startTransition(() => { - setTransactions(cached.data) - }) - return - } - - // Cancel previous request if exists - if (abortControllerRef.current) { - abortControllerRef.current.abort() - } - - // Create new abort controller - abortControllerRef.current = new AbortController() - - setLoading(true) - setError(null) - - try { - const res = await fetch( - `/api/transactions?address=${address}&page=${page}&offset=20`, - { signal: abortControllerRef.current.signal } - ) - const data = await res.json() - - if (data.error) throw new Error(data.error) - - const categorizedData = data.map((tx: Transaction) => ({ - ...tx, - type: categorizeTransaction(tx, address), - })) - - // Update cache - transactionCache.set(cacheKey, { - data: categorizedData, - timestamp: Date.now(), - }) - - startTransition(() => { - setTransactions(categorizedData) - }) - } catch (err: any) { - if (err.name === 'AbortError') return - console.error("Error fetching transactions:", err) - setError(err.message || "Failed to fetch transactions") - } finally { - setLoading(false) - abortControllerRef.current = null - } - }, [address, page, cacheKey]) - - // Cleanup effect - useEffect(() => { - return () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort() - } - } - }, []) - - // Debounced fetch effect - useEffect(() => { - const timer = setTimeout(fetchTransactions, 300) - return () => clearTimeout(timer) - }, [fetchTransactions]) - - const categorizeTransaction = useCallback((tx: Transaction, userAddress: string): Transaction["type"] => { - if (tx.from === userAddress && tx.to === userAddress) return "swap" - if (tx.from === userAddress) return "outflow" - if (tx.to === userAddress) return "inflow" - return "transfer" - }, []) - - // Memoize filtered transactions with type checking - const filteredTransactions = useMemo(() => ({ - all: transactions, - transfer: transactions.filter((tx) => tx.type === "transfer"), - swap: transactions.filter((tx) => tx.type === "swap"), - inflow: transactions.filter((tx) => tx.type === "inflow"), - outflow: transactions.filter((tx) => tx.type === "outflow"), - }), [transactions]) - - // Memoize table rendering function - const renderTransactionTable = useCallback((transactions: Transaction[]) => ( -
-
- - - From - To - Value - Timestamp - - - -
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const tx = transactions[virtualRow.index] - return ( - - - {tx.from.slice(0, 6)}...{tx.from.slice(-4)} - - - {tx.to.slice(0, 6)}...{tx.to.slice(-4)} - - {tx.value} - {new Date(tx.timestamp).toLocaleString()} - - ) - })} -
-
-
-
- ), [rowVirtualizer]) - - if (loading) { - return ( - - - - - - ) - } - - if (error) { - return ( - - Error - {error} - - ) - } - - if (transactions.length === 0) { - return ( - - No transactions found. - - ) - } - - return ( - - - Recent Transactions - - - - - {Object.entries(TAB_OPTIONS).map(([value, label]) => ( - - {label} - - ))} - - {Object.entries(filteredTransactions).map(([type, txs]) => ( - - {renderTransactionTable(txs)} - - ))} - -
- - -
-
-
- ) -} \ No newline at end of file diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx index 8aa8562..c6367fb 100644 --- a/components/transactions/NetworkStats.tsx +++ b/components/transactions/NetworkStats.tsx @@ -4,7 +4,7 @@ import { useState, useEffect} from 'react' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Clock, Loader2, Gauge, Calculator } from "lucide-react" import axios from 'axios'; -import TransactionTable from '@/components/transactions/TransactionTable'; +import NetworkTransactionTable from '@/components/transactions/NetworkTransactionTable'; interface Stats { transactions24h: number; @@ -168,7 +168,7 @@ export default function TransactionExplorer() {
- +
); diff --git a/components/transactions/NetworkTransactionTable.tsx b/components/transactions/NetworkTransactionTable.tsx new file mode 100644 index 0000000..54c7ad1 --- /dev/null +++ b/components/transactions/NetworkTransactionTable.tsx @@ -0,0 +1,282 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import Link from 'next/link' +import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' +import { toast } from "@/components/ui/use-toast" +import { ethers } from 'ethers'; + +interface Transaction { + hash: string; + method: string; + block: string; + age: string; + from: string; + to: string; + amount: string; + fee: string; + timestamp: number; +} + +export default function NetworkTransactionTable() { + // State variables + const [transactions, setTransactions] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [selectedMethod, setSelectedMethod] = useState(null); + const [totalPages] = useState(5000); + const [isMobile, setIsMobile] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // Etherscan API configuration + const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; + const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; + + interface MethodSignatures { + [key: string]: string; + } + + const knownMethods: MethodSignatures = { + '0xa9059cbb': 'Transfer', + '0x23b872dd': 'TransferFrom', + '0x095ea7b3': 'Approve', + '0x70a08231': 'BalanceOf', + '0x18160ddd': 'TotalSupply', + '0x313ce567': 'Decimals', + '0x06fdde03': 'Name', + '0x95d89b41': 'Symbol', + '0xd0e30db0': 'Deposit', + '0x2e1a7d4d': 'Withdraw', + '0x3593564c': 'Execute', + '0x4a25d94a': 'SwapExactTokensForTokens', + '0x7ff36ab5': 'SwapExactETHForTokens', + '0x791ac947': 'SwapExactTokensForETH', + '0xfb3bdb41': 'SwapETHForExactTokens', + '0x5c11d795': 'SwapTokensForExactTokens', + '0xb6f9de95': 'Claim', + '0x6a627842': 'Mint', + '0xa0712d68': 'Mint', + }; + + const getTransactionMethod = (input: string): string => { + if (input === '0x') return 'Transfer'; + + const functionSelector = input.slice(0, 10).toLowerCase(); + + if (knownMethods[functionSelector]) { + return knownMethods[functionSelector]; + } + + return 'Swap'; + }; + + // Function to get relative time + const getRelativeTime = (timestamp: number) => { + const now = Date.now(); + const diff = now - timestamp * 1000; + + // Ensure diff is not negative + if (diff < 0) return "Just now"; + + const seconds = Math.floor(diff / 1000); + + if (seconds < 60) return `${seconds} secs ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} mins ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hrs ago`; + const days = Math.floor(hours / 24); + return `${days} days ago`; + }; + + // Function to truncate addresses + const truncateAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + + // Fetch latest blocks and their transactions + const fetchLatestTransactions = async () => { + if (!ETHERSCAN_API_KEY) { + console.error('Etherscan API key is not set') + return + } + + try { + setIsLoading(true) + const latestBlockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber') + const latestBlockData = await latestBlockResponse.json() + const latestBlock = parseInt(latestBlockData.result, 16) + + const response = await fetch( + `/api/etherscan?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true` + ) + const data = await response.json() + + if (data.result && data.result.transactions) { + const formattedTransactions = await Promise.all( + data.result.transactions.slice(0, 50).map(async (tx: any) => { + const timestamp = parseInt(data.result.timestamp, 16) + return { + hash: tx.hash, + method: getTransactionMethod(tx.input), + block: parseInt(tx.blockNumber, 16).toString(), + age: getRelativeTime(timestamp), + from: tx.from, + to: tx.to || 'Contract Creation', + amount: ethers.utils.formatEther(tx.value) + ' ETH', + fee: ethers.utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), + timestamp: timestamp + } + }) + ) + setTransactions(formattedTransactions) + } + } catch (error) { + console.error('Error fetching transactions:', error) + toast({ + title: "Error fetching transactions", + description: "Failed to fetch latest transactions.", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + fetchLatestTransactions() + const interval = setInterval(fetchLatestTransactions, 15000) // Refresh every 15 seconds + return () => clearInterval(interval) + }, []) + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768) + } + handleResize() + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text) + toast({ + title: "Copied!", + description: "Address copied to clipboard", + }) + } catch (err) { + toast({ + title: "Failed to copy", + description: "Please try again", + variant: "destructive", + }) + } + } + + return ( +
+ + + + + Txn Hash + Method + Block + Age + From + To + Value + Txn Fee + + + + {isLoading ? ( + + + Loading transactions... + + + ) : ( + transactions.map((tx, index) => ( + + +
+ +
+
+ +
+ + + {truncateAddress(tx.hash)} + + + +
+
+ + + {tx.method} + + + + + + {tx.block} + + + + {tx.age} + +
+ + + {truncateAddress(tx.from)} + + + +
+
+ +
+ + + {truncateAddress(tx.to)} + + + +
+
+ {tx.amount} + {tx.fee} +
+ )) + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/components/transactions/TransactionTable.tsx b/components/transactions/TransactionExplorer.tsx similarity index 100% rename from components/transactions/TransactionTable.tsx rename to components/transactions/TransactionExplorer.tsx diff --git a/components/ui/TransactionTable.tsx b/components/ui/TransactionTable.tsx new file mode 100644 index 0000000..643ba21 --- /dev/null +++ b/components/ui/TransactionTable.tsx @@ -0,0 +1,205 @@ +"use client" + +import { useSearchParams } from "next/navigation" +import { useEffect, useState } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Button } from "@/components/ui/button" +import { Loader2 } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface Transaction { + id: string + from: string + to: string + value: string + timestamp: string + type: "transfer" | "swap" | "inflow" | "outflow" +} + +export default function TransactionTable() { + const searchParams = useSearchParams() + const address = searchParams.get("address") + const [transactions, setTransactions] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [page, setPage] = useState(1) + + useEffect(() => { + if (address) { + setLoading(true) + setError(null) + fetch(`/api/transactions?address=${address}&page=${page}&offset=20`) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + throw new Error(data.error) + } + // Mock categorization of transactions + const categorizedData = data.map((tx: Transaction) => ({ + ...tx, + type: categorizeTransaction(tx, address), + })) + setTransactions(categorizedData) + }) + .catch((err) => { + console.error("Error fetching transactions:", err) + setError(err.message || "Failed to fetch transactions") + }) + .finally(() => setLoading(false)) + } + }, [address, page]) + + const categorizeTransaction = (tx: Transaction, userAddress: string): Transaction["type"] => { + if (tx.from === userAddress && tx.to === userAddress) return "swap" + if (tx.from === userAddress) return "outflow" + if (tx.to === userAddress) return "inflow" + return "transfer" + } + + if (loading) { + return ( + + + + + + ) + } + + if (error) { + return ( + + Error + {error} + + ) + } + + if (transactions.length === 0) { + return ( + + No transactions found. + + ) + } + + const renderTransactionTable = (transactions: Transaction[]) => ( + + + + From + To + Value + Timestamp + + + + {transactions.map((tx) => ( + + + {tx.from.slice(0, 6)}...{tx.from.slice(-4)} + + + {tx.to.slice(0, 6)}...{tx.to.slice(-4)} + + {tx.value} + {new Date(tx.timestamp).toLocaleString()} + + ))} + +
+ ) + + return ( + + + Recent Transactions + + + + + + All + + + Transfer + + + Swap + + + Inflow + + + Outflow + + + {renderTransactionTable(transactions)} + + {renderTransactionTable(transactions.filter((tx) => tx.type === "transfer"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "swap"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "inflow"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "outflow"))} + + +
+ + +
+
+
+ ) +} \ No newline at end of file From 742674012ecc5e53f6b25e21807bb9de4be16b16 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Tue, 11 Mar 2025 20:01:19 +0700 Subject: [PATCH 019/107] readded files --- app/search-offchain/TransactionContent.tsx | 2 +- app/search/TransactionContent.tsx | 97 +-------- components/TransactionTable.tsx | 206 ++++++++++++++++++ .../transactions/NetworkTransactionTable.tsx | 118 +++++----- 4 files changed, 275 insertions(+), 148 deletions(-) create mode 100644 components/TransactionTable.tsx diff --git a/app/search-offchain/TransactionContent.tsx b/app/search-offchain/TransactionContent.tsx index 1c30afb..b2bb5d4 100644 --- a/app/search-offchain/TransactionContent.tsx +++ b/app/search-offchain/TransactionContent.tsx @@ -39,4 +39,4 @@ export default function Transactions() {
) -} +} \ No newline at end of file diff --git a/app/search/TransactionContent.tsx b/app/search/TransactionContent.tsx index d47a0c2..47e6f6f 100644 --- a/app/search/TransactionContent.tsx +++ b/app/search/TransactionContent.tsx @@ -1,75 +1,17 @@ 'use client' -import dynamic from 'next/dynamic' -import { Suspense, useEffect } from 'react' import SearchBar from "@/components/SearchBar" import WalletInfo from "@/components/WalletInfo" -import { Card, CardContent } from "@/components/ui/card" +import TransactionGraph from "@/components/TransactionGraph" +import TransactionTable from "@/components/TransactionTable" +import Portfolio from "@/components/Portfolio" +import NFTGallery from "@/components/NFTGallery" import { useSearchParams } from "next/navigation" -import { Loader2 } from "lucide-react" -// Preload critical components -const FilteredTransactionTable = dynamic(() => import("@/components/ui/TransactionTable"), { - loading: () => Loading transactions..., - ssr: false -}) - -// Defer loading of non-critical components -const TransactionGraph = dynamic(() => import("@/components/TransactionGraph"), { - loading: () => Loading transaction graph..., - ssr: false, -}) - -const Portfolio = dynamic(() => import("@/components/Portfolio"), { - loading: () => Loading portfolio..., - ssr: false, -}) - -const NFTGallery = dynamic(() => import("@/components/NFTGallery"), { - loading: () => Loading NFTs..., - ssr: false, -}) - -// Loading component optimized for frequent reuse -const LoadingCard = ({ children }: { children: React.ReactNode }) => ( - - - -

{children}

-
-
-) export default function Transactions() { const searchParams = useSearchParams() const address = searchParams.get("address") - - // Preload components when address is available - useEffect(() => { - if (address) { - // Preload critical components immediately - const preloadCritical = async () => { - const [FilteredTransactionTable, WalletInfo] = await Promise.all([ - import("@/components/ui/TransactionTable"), - import("@/components/WalletInfo") - ]) - } - preloadCritical() - - // Defer loading of non-critical components - const preloadNonCritical = async () => { - const [TransactionGraph, Portfolio, NFTGallery] = await Promise.all([ - import("@/components/TransactionGraph"), - import("@/components/Portfolio"), - import("@/components/NFTGallery") - ]) - } - // Delay loading non-critical components - const timer = setTimeout(preloadNonCritical, 2000) - return () => clearTimeout(timer) - } - }, [address]) - return (
@@ -78,32 +20,15 @@ export default function Transactions() {
{address ? ( <> - {/* Critical content loaded first */} - Loading transactions...}> - - - - {/* Non-critical content loaded after */} -
+
- Loading wallet info...}> - - - Loading portfolio...}> - - + +
- Loading graph...}> - - -
- - {/* Load NFTs last */} -
- Loading NFTs...}> - - +
+ + ) : (
@@ -116,4 +41,4 @@ export default function Transactions() {
) -} +} \ No newline at end of file diff --git a/components/TransactionTable.tsx b/components/TransactionTable.tsx new file mode 100644 index 0000000..a52d878 --- /dev/null +++ b/components/TransactionTable.tsx @@ -0,0 +1,206 @@ +"use client" + +import { useSearchParams } from "next/navigation" +import { useEffect, useState } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Button } from "@/components/ui/button" +import { Loader2 } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface Transaction { + id: string + from: string + to: string + value: string + timestamp: string + type: "transfer" | "swap" | "inflow" | "outflow" +} + +export default function TransactionTable() { + const searchParams = useSearchParams() + const address = searchParams.get("address") + const [transactions, setTransactions] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [page, setPage] = useState(1) + + useEffect(() => { + if (address) { + setLoading(true) + setError(null) + fetch(`/api/transactions?address=${address}&page=${page}&offset=20`) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + throw new Error(data.error) + } + // Mock categorization of transactions + const categorizedData = data.map((tx: Transaction) => ({ + ...tx, + type: categorizeTransaction(tx, address), + })) + setTransactions(categorizedData) + }) + .catch((err) => { + console.error("Error fetching transactions:", err) + setError(err.message || "Failed to fetch transactions") + }) + .finally(() => setLoading(false)) + } + }, [address, page]) + + const categorizeTransaction = (tx: Transaction, userAddress: string): Transaction["type"] => { + if (tx.from === userAddress && tx.to === userAddress) return "swap" + if (tx.from === userAddress) return "outflow" + if (tx.to === userAddress) return "inflow" + return "transfer" + } + + if (loading) { + return ( + + + + + + ) + } + + if (error) { + return ( + + Error + {error} + + ) + } + + if (transactions.length === 0) { + return ( + + No transactions found. + + ) + } + + const renderTransactionTable = (transactions: Transaction[]) => ( + + + + From + To + Value + Timestamp + + + + {transactions.map((tx) => ( + + + {tx.from.slice(0, 6)}...{tx.from.slice(-4)} + + + {tx.to.slice(0, 6)}...{tx.to.slice(-4)} + + {tx.value} + {new Date(tx.timestamp).toLocaleString()} + + ))} + +
+ ) + + return ( + + + Recent Transactions + + + + + + All + + + Transfer + + + Swap + + + Inflow + + + Outflow + + + {renderTransactionTable(transactions)} + + {renderTransactionTable(transactions.filter((tx) => tx.type === "transfer"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "swap"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "inflow"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "outflow"))} + + +
+ + +
+
+
+ ) +} + diff --git a/components/transactions/NetworkTransactionTable.tsx b/components/transactions/NetworkTransactionTable.tsx index 54c7ad1..224f65a 100644 --- a/components/transactions/NetworkTransactionTable.tsx +++ b/components/transactions/NetworkTransactionTable.tsx @@ -21,17 +21,10 @@ interface Transaction { } export default function NetworkTransactionTable() { - // State variables const [transactions, setTransactions] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [selectedMethod, setSelectedMethod] = useState(null); - const [totalPages] = useState(5000); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const [isMobile, setIsMobile] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; - const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; interface MethodSignatures { [key: string]: string; @@ -61,26 +54,15 @@ export default function NetworkTransactionTable() { const getTransactionMethod = (input: string): string => { if (input === '0x') return 'Transfer'; - const functionSelector = input.slice(0, 10).toLowerCase(); - - if (knownMethods[functionSelector]) { - return knownMethods[functionSelector]; - } - - return 'Swap'; + return knownMethods[functionSelector] || 'Swap'; }; - // Function to get relative time const getRelativeTime = (timestamp: number) => { const now = Date.now(); const diff = now - timestamp * 1000; - - // Ensure diff is not negative if (diff < 0) return "Just now"; - const seconds = Math.floor(diff / 1000); - if (seconds < 60) return `${seconds} secs ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes} mins ago`; @@ -90,33 +72,29 @@ export default function NetworkTransactionTable() { return `${days} days ago`; }; - // Function to truncate addresses const truncateAddress = (address: string) => { return `${address.slice(0, 6)}...${address.slice(-4)}`; }; - // Fetch latest blocks and their transactions const fetchLatestTransactions = async () => { - if (!ETHERSCAN_API_KEY) { - console.error('Etherscan API key is not set') - return - } - try { - setIsLoading(true) - const latestBlockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber') - const latestBlockData = await latestBlockResponse.json() - const latestBlock = parseInt(latestBlockData.result, 16) + setIsLoading(true); + setError(null); + const latestBlockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber'); + if (!latestBlockResponse.ok) throw new Error('Failed to fetch latest block'); + const latestBlockData = await latestBlockResponse.json(); + const response = await fetch( - `/api/etherscan?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true` - ) - const data = await response.json() + `/api/etherscan?module=proxy&action=eth_getBlockByNumber&tag=${latestBlockData.result}&boolean=true` + ); + if (!response.ok) throw new Error('Failed to fetch block transactions'); + const data = await response.json(); if (data.result && data.result.transactions) { const formattedTransactions = await Promise.all( data.result.transactions.slice(0, 50).map(async (tx: any) => { - const timestamp = parseInt(data.result.timestamp, 16) + const timestamp = parseInt(data.result.timestamp, 16); return { hash: tx.hash, method: getTransactionMethod(tx.input), @@ -127,52 +105,61 @@ export default function NetworkTransactionTable() { amount: ethers.utils.formatEther(tx.value) + ' ETH', fee: ethers.utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), timestamp: timestamp - } + }; }) - ) - setTransactions(formattedTransactions) + ); + setTransactions(formattedTransactions); } } catch (error) { - console.error('Error fetching transactions:', error) + console.error('Error fetching transactions:', error); + setError('Failed to fetch transactions'); toast({ - title: "Error fetching transactions", - description: "Failed to fetch latest transactions.", + title: "Error", + description: "Failed to fetch latest transactions. Please try again later.", variant: "destructive", - }) + }); } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; useEffect(() => { - fetchLatestTransactions() - const interval = setInterval(fetchLatestTransactions, 15000) // Refresh every 15 seconds - return () => clearInterval(interval) - }, []) + fetchLatestTransactions(); + const interval = setInterval(fetchLatestTransactions, 15000); + return () => clearInterval(interval); + }, []); useEffect(() => { const handleResize = () => { - setIsMobile(window.innerWidth < 768) - } - handleResize() - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) - }, []) + setIsMobile(window.innerWidth < 768); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); const copyToClipboard = async (text: string) => { try { - await navigator.clipboard.writeText(text) + await navigator.clipboard.writeText(text); toast({ title: "Copied!", description: "Address copied to clipboard", - }) + }); } catch (err) { toast({ title: "Failed to copy", description: "Please try again", variant: "destructive", - }) + }); } + }; + + if (error) { + return ( +
+ {error} +
+ ); } return ( @@ -194,8 +181,17 @@ export default function NetworkTransactionTable() { {isLoading ? ( - - Loading transactions... + +
+
+ Loading transactions... +
+
+
+ ) : transactions.length === 0 ? ( + + + No transactions found ) : ( @@ -278,5 +274,5 @@ export default function NetworkTransactionTable() {
- ) + ); } \ No newline at end of file From ad82a4b48cb6308dd77bdb93a8945425dccbe286 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Tue, 11 Mar 2025 20:22:16 +0700 Subject: [PATCH 020/107] fix bug --- app/transactions/page.tsx | 13 +- components/transactions/NetworkStats.tsx | 170 ++++++++++++----------- components/ui/TransactionTable.tsx | 16 +-- 3 files changed, 103 insertions(+), 96 deletions(-) diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 77831bd..c6c8ad4 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -8,7 +8,6 @@ import ParticlesBackground from '@/components/ParticlesBackground'; import RevenueGraph from '@/components/transactions/RevenueGraph'; import { Skeleton } from "@/components/ui/skeleton" import WalletCharts from '@/components/transactions/WalletCharts'; -import TransactionTable from '@/components/transactions/NetworkTransactionTable'; export default function TransactionExplorer() { return ( @@ -19,10 +18,16 @@ export default function TransactionExplorer() { {/* Main Content */}
- + }> + +
- - + }> + + + }> + +
diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx index c6367fb..4b8aecb 100644 --- a/components/transactions/NetworkStats.tsx +++ b/components/transactions/NetworkStats.tsx @@ -2,40 +2,46 @@ import { useState, useEffect} from 'react' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Clock, Loader2, Gauge, Calculator } from "lucide-react" import axios from 'axios'; -import NetworkTransactionTable from '@/components/transactions/NetworkTransactionTable'; +import TransactionTable from '@/components/ui/TransactionTable'; interface Stats { transactions24h: number; pendingTransactions: number; networkFee: number; avgGasFee: number; - totalTransactionAmount: number; + totalTransactionAmount: number; // New field for total transaction amount } +// Initial state const initialStats: Stats = { transactions24h: 0, pendingTransactions: 0, networkFee: 0, avgGasFee: 0, - totalTransactionAmount: 0, + totalTransactionAmount: 0, // Initialize to 0 }; export default function TransactionExplorer() { + // State variables const [, setIsMobile] = useState(false); const [stats, setStats] = useState(initialStats); const [, setTotalTransactions] = useState(0); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [, setLoading] = useState(true); + const [, setError] = useState(null); - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; - const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; + // Etherscan API configuration + const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key + const API_URL = `/api/etherscan?module=proxy&action=eth_blockNumber`; + + // Fetch network statistics const fetchNetworkStats = async () => { try { - // Fetch gas prices - const gasResponse = await fetch('/api/etherscan?module=gastracker&action=gasoracle'); + // Get gas price statistics + const gasResponse = await fetch( + `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` + ); const gasData = await gasResponse.json(); if (gasData.status === "1") { @@ -46,65 +52,68 @@ export default function TransactionExplorer() { })); } - // Fetch latest block number - const blockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber'); + // Get 24h transaction count (approximate) + const blockResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` + ); const blockData = await blockResponse.json(); const latestBlock = parseInt(blockData.result, 16); - const blocksIn24h = Math.floor(86400 / 15); // Approximate blocks in 24h + // Assuming ~15 second block time, calculate blocks in 24h + const blocksIn24h = Math.floor(86400 / 15); - // Fetch transaction count + // Get transaction count for latest block const txCountResponse = await fetch( - `/api/etherscan?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}` + `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` ); const txCountData = await txCountResponse.json(); const txCount = parseInt(txCountData.result, 16); - // Fetch pending transactions - const pendingResponse = await fetch('/api/pending'); - const pendingData = await pendingResponse.json(); - setStats(prev => ({ ...prev, - transactions24h: txCount * blocksIn24h, - pendingTransactions: pendingData.pendingTransactions || 0 + transactions24h: txCount * blocksIn24h, // Rough estimation + pendingTransactions: txCount // Current block's transaction count as pending })); } catch (error) { console.error('Error fetching network stats:', error); - setError('Failed to fetch network stats'); - } finally { - setLoading(false); } }; const fetchTotalTransactions = async () => { - setLoading(true); + setLoading(true); // Đặt loading thành true trước khi gọi API try { const response = await axios.get(API_URL); - const totalTxCount = response.data.result; + const totalTxCount = response.data.result; // Giả định bạn có cách lấy số giao dịch từ API + setTotalTransactions(Number(totalTxCount)); } catch (err) { setError('Lỗi khi lấy dữ liệu từ API'); } finally { setLoading(false); } - }; +}; - useEffect(() => { +useEffect(() => { fetchTotalTransactions(); const interval = setInterval(() => { fetchTotalTransactions(); }, 300000); return () => clearInterval(interval); - }, []); +}, []); + useEffect(() => { fetchNetworkStats(); - const interval = setInterval(fetchNetworkStats, 30000); // Update every 30 seconds + const interval = setInterval(() => { + fetchNetworkStats(); + }, 30000); // Refresh every 5 minutes + return () => clearInterval(interval); }, []); + + // Effect to handle responsive design useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth < 768); @@ -114,62 +123,55 @@ export default function TransactionExplorer() { return () => window.removeEventListener('resize', handleResize); }, []); + return ( -
-
-
- - -
- - Transactions (24h) -
-
- -

- {stats.transactions24h.toLocaleString()} -

-
-
- - - -
- - Pending Txns -
-
- -

{stats.pendingTransactions.toLocaleString()}

-
-
- - - -
- - Network Fee -
-
- -

{stats.networkFee.toFixed(2)} Gwei

-
-
- - - -
- - AVG Gas Fee -
-
- -

{stats.avgGasFee.toFixed(2)} Gwei

-
-
+
+
+ {/* Statistics cards */} +
+ + Transactions (24h) + + +

+ {stats.transactions24h.toLocaleString()} +

+
+
+ + + + Pending Txns + + +

{stats.pendingTransactions.toLocaleString()}

+
+
+ + + + Network Fee + + +

{stats.networkFee.toFixed(2)} Gwei

+
+
+ + + + AVG Gas Fee + + +

{stats.avgGasFee.toFixed(2)} Gwei

+
+
- + +
); -} \ No newline at end of file +} + + + \ No newline at end of file diff --git a/components/ui/TransactionTable.tsx b/components/ui/TransactionTable.tsx index 643ba21..181dee9 100644 --- a/components/ui/TransactionTable.tsx +++ b/components/ui/TransactionTable.tsx @@ -176,9 +176,9 @@ export default function TransactionTable() {
- - +
From e23c8e496c16f9fd6a3d41211c564ef1109ec1da Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Tue, 11 Mar 2025 20:37:52 +0700 Subject: [PATCH 021/107] retry add transaction table --- app/api/etherscan/route.ts | 60 +++++- app/transactions/page.tsx | 42 +++- components/transactions/NetworkStats.tsx | 246 ++++++++++++----------- 3 files changed, 216 insertions(+), 132 deletions(-) diff --git a/app/api/etherscan/route.ts b/app/api/etherscan/route.ts index 7d8bac3..ea1145d 100644 --- a/app/api/etherscan/route.ts +++ b/app/api/etherscan/route.ts @@ -1,16 +1,68 @@ import { NextResponse } from "next/server" +// Simple in-memory cache +const cache = new Map(); +const CACHE_DURATION = 5000; // 5 seconds cache +let lastCallTimestamp = 0; +const RATE_LIMIT_WINDOW = 200; // 200ms between calls (5 calls per second) + export async function GET(request: Request) { const { searchParams } = new URL(request.url) - const moduleParam = searchParams.get("module") - const action = searchParams.get("action") - const url = `https://api.etherscan.io/api?module=${moduleParam}&action=${action}&apikey=${process.env.ETHERSCAN_API_KEY}` + // Create cache key from the entire URL + const cacheKey = searchParams.toString(); + + // Check cache + const cachedData = cache.get(cacheKey); + if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) { + return NextResponse.json(cachedData.data); + } + + // Rate limiting + const now = Date.now(); + if (now - lastCallTimestamp < RATE_LIMIT_WINDOW) { + await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_WINDOW)); + } + lastCallTimestamp = Date.now(); + + // Build the Etherscan API URL with all parameters + const urlParams = new URLSearchParams() + + // Add all search params to the URL + searchParams.forEach((value, key) => { + urlParams.append(key, value) + }) + + // Always include the API key + urlParams.append('apikey', process.env.ETHERSCAN_API_KEY || '') + + const url = `https://api.etherscan.io/api?${urlParams.toString()}` + try { const response = await fetch(url) + if (!response.ok) { + throw new Error(`Etherscan API responded with status: ${response.status}`) + } const data = await response.json() + + // Check for Etherscan API errors + if (data.status === "0" && data.message === "NOTOK") { + throw new Error(data.result) + } + + // Cache the successful response + cache.set(cacheKey, { data, timestamp: Date.now() }); + return NextResponse.json(data) } catch (error) { - return NextResponse.json({ error: "Failed to fetch from Etherscan" }, { status: 500 }) + console.error('Etherscan API error:', error) + return NextResponse.json( + { + status: "0", + message: "NOTOK", + result: error instanceof Error ? error.message : "Failed to fetch from Etherscan" + }, + { status: 500 } + ) } } diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index c6c8ad4..c5442fa 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -1,13 +1,31 @@ 'use client'; -import Link from 'next/link'; -import { Metadata } from "next" import { Suspense } from "react" import NetworkStats from '@/components/transactions/NetworkStats'; import ParticlesBackground from '@/components/ParticlesBackground'; import RevenueGraph from '@/components/transactions/RevenueGraph'; -import { Skeleton } from "@/components/ui/skeleton" import WalletCharts from '@/components/transactions/WalletCharts'; +import { Card, CardContent } from "@/components/ui/card"; +import { Loader2 } from "lucide-react"; + +// Loading component +const LoadingCard = ({ children }: { children: React.ReactNode }) => ( + + + +

{children}

+
+
+); + +// Error boundary component +const ErrorCard = ({ error }: { error: string }) => ( + + + {error} + + +); export default function TransactionExplorer() { return ( @@ -15,17 +33,23 @@ export default function TransactionExplorer() {
- {/* Main Content */}
+ {/* Revenue Graph */}
- }> + Loading revenue graph...}>
- }> - - - }> + + {/* Wallet Charts */} +
+ Loading wallet charts...}> + + +
+ + {/* Network Stats and Transaction Table */} + Loading network stats...}>
diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx index 4b8aecb..ffaf6b2 100644 --- a/components/transactions/NetworkStats.tsx +++ b/components/transactions/NetworkStats.tsx @@ -1,49 +1,53 @@ 'use client' -import { useState, useEffect} from 'react' +import { useState, useEffect } from 'react' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import axios from 'axios'; -import TransactionTable from '@/components/ui/TransactionTable'; +import { Clock, Loader2, Gauge, Calculator } from "lucide-react" +import { toast } from "@/components/ui/use-toast" +import NetworkTransactionTable from '@/components/transactions/NetworkTransactionTable'; interface Stats { transactions24h: number; pendingTransactions: number; networkFee: number; avgGasFee: number; - totalTransactionAmount: number; // New field for total transaction amount + totalTransactionAmount: number; } -// Initial state const initialStats: Stats = { transactions24h: 0, pendingTransactions: 0, networkFee: 0, avgGasFee: 0, - totalTransactionAmount: 0, // Initialize to 0 + totalTransactionAmount: 0, }; -export default function TransactionExplorer() { - // State variables - const [, setIsMobile] = useState(false); +export default function NetworkStats() { const [stats, setStats] = useState(initialStats); - const [, setTotalTransactions] = useState(0); - const [, setLoading] = useState(true); - const [, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [updateKey, setUpdateKey] = useState(0); // Used to trigger table updates - - // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key - const API_URL = `/api/etherscan?module=proxy&action=eth_blockNumber`; - - // Fetch network statistics const fetchNetworkStats = async () => { try { - // Get gas price statistics - const gasResponse = await fetch( - `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` - ); + setLoading(true); + setError(null); + + // Batch API calls together + const [gasResponse, blockResponse] = await Promise.all([ + fetch('/api/etherscan?module=gastracker&action=gasoracle'), + fetch('/api/etherscan?module=proxy&action=eth_blockNumber') + ]); + + // Handle gas price data + if (!gasResponse.ok) throw new Error('Failed to fetch gas prices'); const gasData = await gasResponse.json(); + // Handle block number data + if (!blockResponse.ok) throw new Error('Failed to fetch latest block'); + const blockData = await blockResponse.json(); + + // Process gas data if (gasData.status === "1") { setStats(prev => ({ ...prev, @@ -52,126 +56,130 @@ export default function TransactionExplorer() { })); } - // Get 24h transaction count (approximate) - const blockResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` - ); - const blockData = await blockResponse.json(); + // Process block data const latestBlock = parseInt(blockData.result, 16); - - // Assuming ~15 second block time, calculate blocks in 24h - const blocksIn24h = Math.floor(86400 / 15); + const blocksIn24h = Math.floor(86400 / 15); // Approximate blocks in 24h - // Get transaction count for latest block + // Fetch transaction count with delay to respect rate limit + await new Promise(resolve => setTimeout(resolve, 200)); const txCountResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` + `/api/etherscan?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}` ); + + if (!txCountResponse.ok) throw new Error('Failed to fetch transaction count'); const txCountData = await txCountResponse.json(); const txCount = parseInt(txCountData.result, 16); + // Update stats setStats(prev => ({ ...prev, - transactions24h: txCount * blocksIn24h, // Rough estimation - pendingTransactions: txCount // Current block's transaction count as pending + transactions24h: txCount * blocksIn24h, + pendingTransactions: Math.floor(Math.random() * 100) + 50 // Temporary mock data for pending transactions })); + + // Trigger table update + setUpdateKey(prev => prev + 1); } catch (error) { console.error('Error fetching network stats:', error); - } - }; - - const fetchTotalTransactions = async () => { - setLoading(true); // Đặt loading thành true trước khi gọi API - try { - const response = await axios.get(API_URL); - const totalTxCount = response.data.result; // Giả định bạn có cách lấy số giao dịch từ API - - setTotalTransactions(Number(totalTxCount)); - } catch (err) { - setError('Lỗi khi lấy dữ liệu từ API'); + setError('Failed to fetch network stats'); + toast({ + title: "Error", + description: "Failed to fetch network stats. Please try again later.", + variant: "destructive", + }); } finally { - setLoading(false); + setLoading(false); } -}; - -useEffect(() => { - fetchTotalTransactions(); - const interval = setInterval(() => { - fetchTotalTransactions(); - }, 300000); - - return () => clearInterval(interval); -}, []); - + }; useEffect(() => { fetchNetworkStats(); - const interval = setInterval(() => { - fetchNetworkStats(); - }, 30000); // Refresh every 5 minutes - + const interval = setInterval(fetchNetworkStats, 15000); // Update every 15 seconds return () => clearInterval(interval); }, []); - - // Effect to handle responsive design - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return ( -
-
- {/* Statistics cards */} -
- - Transactions (24h) - - -

- {stats.transactions24h.toLocaleString()} -

-
-
- - - - Pending Txns - - -

{stats.pendingTransactions.toLocaleString()}

-
-
- - - - Network Fee - - -

{stats.networkFee.toFixed(2)} Gwei

-
-
- - - - AVG Gas Fee - - -

{stats.avgGasFee.toFixed(2)} Gwei

-
-
+
+
+
+ + +
+ + Transactions (24h) +
+
+ +

+ {loading ? ( + + ) : ( + stats.transactions24h.toLocaleString() + )} +

+
+
+ + + +
+ + Pending Txns +
+
+ +

+ {loading ? ( + + ) : ( + stats.pendingTransactions.toLocaleString() + )} +

+
+
+ + + +
+ + Network Fee +
+
+ +

+ {loading ? ( + + ) : ( + `${stats.networkFee.toFixed(2)} Gwei` + )} +

+
+
+ + + +
+ + AVG Gas Fee +
+
+ +

+ {loading ? ( + + ) : ( + `${stats.avgGasFee.toFixed(2)} Gwei` + )} +

+
+
- + {/* Transaction Table */} +
+ +
); -} - - - \ No newline at end of file +} \ No newline at end of file From dd917b7db70b6f57e5ac006c26a83b5fb2e06c78 Mon Sep 17 00:00:00 2001 From: Minh Duy - Mordred <95609626+TTMordred@users.noreply.github.com> Date: Tue, 11 Mar 2025 21:05:30 +0700 Subject: [PATCH 022/107] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index df6990d..29d81cc 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ SMTP_PASSWORD=your-password NEO4J_URI=neo4j+s://your-database-uri NEO4J_USERNAME=your-username NEO4J_PASSWORD=your-password +NEXTAUTH_URL=https://cryptopath.vercel.app/ +NEXTAUTH_SECRET=your-secret-key ``` ```bash # Start the development server From 49b1e534b0d1a37ef621c161337da9593f95ab3c Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 12 Mar 2025 01:52:48 +0700 Subject: [PATCH 023/107] oke --- components/ParticlesBackground.tsx | 4 +- package-lock.json | 80 +++++++++++++++--------------- package.json | 2 +- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/components/ParticlesBackground.tsx b/components/ParticlesBackground.tsx index caa38b8..5db6359 100644 --- a/components/ParticlesBackground.tsx +++ b/components/ParticlesBackground.tsx @@ -20,7 +20,7 @@ const particlesConfig = { }, }, color: { - value: "FF0000", + value: "ffc259", }, shape: { type: "circle", @@ -36,7 +36,7 @@ const particlesConfig = { line_linked: { enable: true, distance: 150, - color: "#FF0000", + color: "#ffcd59", opacity: 0.4, width: 1, }, diff --git a/package-lock.json b/package-lock.json index 77d4d7d..cf52a29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "loading-spinner": "^1.2.1", "lucide-react": "^0.475.0", "neo4j-driver": "^5.28.1", - "next": "^15.2.1", + "next": "^15.2.2", "nodemailer": "^6.10.0", "particles.js": "^2.0.0", "pino-pretty": "^13.0.0", @@ -2858,9 +2858,9 @@ } }, "node_modules/@next/env": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.1.tgz", - "integrity": "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.2.tgz", + "integrity": "sha512-yWgopCfA9XDR8ZH3taB5nRKtKJ1Q5fYsTOuYkzIIoS8TJ0UAUKAGF73JnGszbjk2ufAQDj6mDdgsJAFx5CLtYQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2874,9 +2874,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.1.tgz", - "integrity": "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz", + "integrity": "sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==", "cpu": [ "arm64" ], @@ -2890,9 +2890,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.1.tgz", - "integrity": "sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz", + "integrity": "sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==", "cpu": [ "x64" ], @@ -2906,9 +2906,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.1.tgz", - "integrity": "sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz", + "integrity": "sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==", "cpu": [ "arm64" ], @@ -2922,9 +2922,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.1.tgz", - "integrity": "sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz", + "integrity": "sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==", "cpu": [ "arm64" ], @@ -2938,9 +2938,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.1.tgz", - "integrity": "sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz", + "integrity": "sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==", "cpu": [ "x64" ], @@ -2954,9 +2954,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.1.tgz", - "integrity": "sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz", + "integrity": "sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==", "cpu": [ "x64" ], @@ -2970,9 +2970,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.1.tgz", - "integrity": "sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz", + "integrity": "sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==", "cpu": [ "arm64" ], @@ -2986,9 +2986,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.1.tgz", - "integrity": "sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.2.tgz", + "integrity": "sha512-52nWy65S/R6/kejz3jpvHAjZDPKIbEQu4x9jDBzmB9jJfuOy5rspjKu4u77+fI4M/WzLXrrQd57hlFGzz1ubcQ==", "cpu": [ "x64" ], @@ -14643,12 +14643,12 @@ "license": "Apache-2.0" }, "node_modules/next": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.1.tgz", - "integrity": "sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==", + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.2.tgz", + "integrity": "sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA==", "license": "MIT", "dependencies": { - "@next/env": "15.2.1", + "@next/env": "15.2.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -14663,14 +14663,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.1", - "@next/swc-darwin-x64": "15.2.1", - "@next/swc-linux-arm64-gnu": "15.2.1", - "@next/swc-linux-arm64-musl": "15.2.1", - "@next/swc-linux-x64-gnu": "15.2.1", - "@next/swc-linux-x64-musl": "15.2.1", - "@next/swc-win32-arm64-msvc": "15.2.1", - "@next/swc-win32-x64-msvc": "15.2.1", + "@next/swc-darwin-arm64": "15.2.2", + "@next/swc-darwin-x64": "15.2.2", + "@next/swc-linux-arm64-gnu": "15.2.2", + "@next/swc-linux-arm64-musl": "15.2.2", + "@next/swc-linux-x64-gnu": "15.2.2", + "@next/swc-linux-x64-musl": "15.2.2", + "@next/swc-win32-arm64-msvc": "15.2.2", + "@next/swc-win32-x64-msvc": "15.2.2", "sharp": "^0.33.5" }, "peerDependencies": { diff --git a/package.json b/package.json index 9cec33d..40d593f 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "loading-spinner": "^1.2.1", "lucide-react": "^0.475.0", "neo4j-driver": "^5.28.1", - "next": "^15.2.1", + "next": "^15.2.2", "nodemailer": "^6.10.0", "particles.js": "^2.0.0", "pino-pretty": "^13.0.0", From 68e1f7060b3d7da8d49ad17c7080b7a52cf6a6d1 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 12 Mar 2025 01:56:17 +0700 Subject: [PATCH 024/107] Update particle colors in ParticlesBackground component --- components/ParticlesBackground.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ParticlesBackground.tsx b/components/ParticlesBackground.tsx index 5db6359..53ad0a1 100644 --- a/components/ParticlesBackground.tsx +++ b/components/ParticlesBackground.tsx @@ -20,7 +20,7 @@ const particlesConfig = { }, }, color: { - value: "ffc259", + value: "#f5b056", }, shape: { type: "circle", @@ -36,7 +36,7 @@ const particlesConfig = { line_linked: { enable: true, distance: 150, - color: "#ffcd59", + color: "#ffc259", opacity: 0.4, width: 1, }, From af65c98e3253fb1bbf40445fde162979d9e87b3d Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 20:48:13 +0700 Subject: [PATCH 025/107] Add Supabase integration and caching for global data; update styles and clean up code --- README.md | 2 - app/api/etherscan/route.ts | 60 +- app/api/pending/route.ts | 31 - app/globals.css | 2 +- app/layout.tsx | 33 +- app/login/page.tsx | 222 ++++-- app/search-offchain/TransactionContent.tsx | 2 +- app/search/TransactionContent.tsx | 2 +- app/signup/page.tsx | 92 ++- app/transactions/page.tsx | 47 +- components/Header.tsx | 117 ++- components/ParticlesBackground.tsx | 2 +- components/table/ClientContent.tsx | 1 - components/table/CoinCard.tsx | 2 - components/transactions/NetworkStats.tsx | 185 ----- .../transactions/NetworkTransactionTable.tsx | 278 ------- components/transactions/RevenueGraph.tsx | 85 --- .../transactions/TransactionExplorer.tsx | 500 ------------- components/transactions/WalletCharts.tsx | 296 -------- components/ui/NetworkStats.tsx | 177 +++++ components/ui/TransactionTable.tsx | 658 +++++++++++----- lib/api/coinApi.ts | 77 +- lib/api/globalApi.ts | 38 +- lib/context/AuthContext.tsx | 297 ++++++++ lib/types.ts | 334 ++++----- next.config.js | 31 - next.config.js.backup | 31 - package-lock.json | 703 ++++++++---------- package.json | 7 +- src/integrations/supabase/client.ts | 11 + src/integrations/supabase/types.ts | 210 ++++++ supabase/config.toml | 1 + 32 files changed, 2066 insertions(+), 2468 deletions(-) delete mode 100644 app/api/pending/route.ts delete mode 100644 components/transactions/NetworkStats.tsx delete mode 100644 components/transactions/NetworkTransactionTable.tsx delete mode 100644 components/transactions/RevenueGraph.tsx delete mode 100644 components/transactions/TransactionExplorer.tsx delete mode 100644 components/transactions/WalletCharts.tsx create mode 100644 components/ui/NetworkStats.tsx create mode 100644 lib/context/AuthContext.tsx delete mode 100644 next.config.js delete mode 100644 next.config.js.backup create mode 100644 src/integrations/supabase/client.ts create mode 100644 src/integrations/supabase/types.ts create mode 100644 supabase/config.toml diff --git a/README.md b/README.md index 29d81cc..df6990d 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,6 @@ SMTP_PASSWORD=your-password NEO4J_URI=neo4j+s://your-database-uri NEO4J_USERNAME=your-username NEO4J_PASSWORD=your-password -NEXTAUTH_URL=https://cryptopath.vercel.app/ -NEXTAUTH_SECRET=your-secret-key ``` ```bash # Start the development server diff --git a/app/api/etherscan/route.ts b/app/api/etherscan/route.ts index ea1145d..7d8bac3 100644 --- a/app/api/etherscan/route.ts +++ b/app/api/etherscan/route.ts @@ -1,68 +1,16 @@ import { NextResponse } from "next/server" -// Simple in-memory cache -const cache = new Map(); -const CACHE_DURATION = 5000; // 5 seconds cache -let lastCallTimestamp = 0; -const RATE_LIMIT_WINDOW = 200; // 200ms between calls (5 calls per second) - export async function GET(request: Request) { const { searchParams } = new URL(request.url) + const moduleParam = searchParams.get("module") + const action = searchParams.get("action") - // Create cache key from the entire URL - const cacheKey = searchParams.toString(); - - // Check cache - const cachedData = cache.get(cacheKey); - if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) { - return NextResponse.json(cachedData.data); - } - - // Rate limiting - const now = Date.now(); - if (now - lastCallTimestamp < RATE_LIMIT_WINDOW) { - await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_WINDOW)); - } - lastCallTimestamp = Date.now(); - - // Build the Etherscan API URL with all parameters - const urlParams = new URLSearchParams() - - // Add all search params to the URL - searchParams.forEach((value, key) => { - urlParams.append(key, value) - }) - - // Always include the API key - urlParams.append('apikey', process.env.ETHERSCAN_API_KEY || '') - - const url = `https://api.etherscan.io/api?${urlParams.toString()}` - + const url = `https://api.etherscan.io/api?module=${moduleParam}&action=${action}&apikey=${process.env.ETHERSCAN_API_KEY}` try { const response = await fetch(url) - if (!response.ok) { - throw new Error(`Etherscan API responded with status: ${response.status}`) - } const data = await response.json() - - // Check for Etherscan API errors - if (data.status === "0" && data.message === "NOTOK") { - throw new Error(data.result) - } - - // Cache the successful response - cache.set(cacheKey, { data, timestamp: Date.now() }); - return NextResponse.json(data) } catch (error) { - console.error('Etherscan API error:', error) - return NextResponse.json( - { - status: "0", - message: "NOTOK", - result: error instanceof Error ? error.message : "Failed to fetch from Etherscan" - }, - { status: 500 } - ) + return NextResponse.json({ error: "Failed to fetch from Etherscan" }, { status: 500 }) } } diff --git a/app/api/pending/route.ts b/app/api/pending/route.ts deleted file mode 100644 index cf3d675..0000000 --- a/app/api/pending/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextResponse } from "next/server" - -const ETHERSCAN_API_URL = "https://api.etherscan.io/api" - -export async function GET() { - try { - const response = await fetch( - `${ETHERSCAN_API_URL}?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=pending&apikey=${process.env.ETHERSCAN_API_KEY}` - ) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const data = await response.json() - - if (data.status !== "1" && !data.result) { - throw new Error(data.message || "Etherscan API returned an error") - } - - const pendingTxCount = parseInt(data.result, 16) - - return NextResponse.json({ pendingTransactions: pendingTxCount }) - } catch (error) { - console.error("Error fetching pending transactions:", error) - return NextResponse.json( - { error: error instanceof Error ? error.message : "An unknown error occurred" }, - { status: 500 } - ) - } -} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index b4bfd71..9caeb36 100644 --- a/app/globals.css +++ b/app/globals.css @@ -63,7 +63,7 @@ } .cp-button--primary { - @apply bg-[#F5B056] text-black hover:bg-gray-200; + @apply bg-[#F5B056] text-black hover:bg-[#ff6500]; } .cp-button--secondary { diff --git a/app/layout.tsx b/app/layout.tsx index da6081e..1a34c90 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,6 +8,7 @@ import QueryProvider from "./QueryProvider"; // ✅ Import Client Component import "./globals.css"; import { Toaster } from 'react-hot-toast'; import { WalletProvider } from '@/components/Faucet/walletcontext'; // Thêm WalletProvider +import { AuthProvider } from '@/lib/context/AuthContext'; const geistSans = Geist({ variable: "--font-geist-sans", @@ -45,26 +46,22 @@ export const metadata: Metadata = { }, }; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {/* Bao bọc bằng WalletProvider */} - {/* ✅ Bọc bên trong Client Component */} - -
- {children} - -
- - + + + + + +
+ {children} + +
+ + + ); -} \ No newline at end of file +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 8948fe7..fa09f0b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,8 +1,11 @@ + 'use client'; import Link from 'next/link'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import ParticlesBackground from '@/components/ParticlesBackground'; +import { toast } from 'sonner'; +import { supabase } from '@/src/integrations/supabase/client'; import { Web3OnboardProvider, init, useConnectWallet } from '@web3-onboard/react'; import injectedModule from '@web3-onboard/injected-wallets'; import walletConnectModule from '@web3-onboard/walletconnect'; @@ -12,13 +15,14 @@ import safeModule from '@web3-onboard/gnosis' import trezorModule from '@web3-onboard/trezor' import magicModule from '@web3-onboard/magic' import dcentModule from '@web3-onboard/dcent'; - -const dcent = dcentModule(); import sequenceModule from '@web3-onboard/sequence' import tahoModule from '@web3-onboard/taho' import trustModule from '@web3-onboard/trust' import okxModule from '@web3-onboard/okx' import frontierModule from '@web3-onboard/frontier'; +import { useAuth } from '@/lib/context/AuthContext'; + +const dcent = dcentModule(); const INFURA_KEY = '7d389678fba04ceb9510b2be4fff5129'; // Replace with your Infura key @@ -28,7 +32,6 @@ const walletConnect = walletConnectModule({ optionalChains: [1, 137] // Optional: specify chains you want to support }); - const injected = injectedModule(); const coinbase = coinbaseModule(); const infinityWallet = infinityWalletModule() @@ -121,7 +124,6 @@ const chains = [ const appMetadata = { name: 'CryptoPath', - //icon: '', // Replace with your actual icon description: 'Login to CryptoPath with your wallet', recommendedInjectedWallets: [ { name: 'MetaMask', url: 'https://metamask.io' }, @@ -137,6 +139,7 @@ const web3Onboard = init({ function LoginPageContent() { const router = useRouter(); + const { signInWithWalletConnect, signIn } = useAuth(); // Form state const [email, setEmail] = useState(''); @@ -144,104 +147,121 @@ function LoginPageContent() { const [emailError, setEmailError] = useState(''); const [passwordError, setPasswordError] = useState(''); const [showPassword, setShowPassword] = useState(false); - const [isLoggedOut, setIsLoggedOut] = useState(false); + const [isLoading, setIsLoading] = useState(false); + // Wallet state const [{ wallet, connecting }, connect, disconnect] = useConnectWallet(); + const [isLoggedOut, setIsLoggedOut] = useState(false); + interface Account { address: string; ens: string | null; } + const formatWalletAddress = (walletAddress: string) => { if (!walletAddress) return ""; return `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`; }; + const [account, setAccount] = useState(null); // Handle wallet connection useEffect(() => { - if (wallet?.provider && !isLoggedOut) { // Chỉ đăng nhập nếu chưa logout + if (wallet?.provider && !isLoggedOut) { const { address, ens } = wallet.accounts[0]; setAccount({ address, ens: ens?.name || null, }); - const userData = { - walletAddress: address, - name: ens?.name || formatWalletAddress(address), // Sử dụng ENS nếu có, nếu không thì dùng địa chỉ ví rút gọn + + // Store wallet authentication details in Supabase using our custom AuthContext method + const authenticateWithWallet = async () => { + try { + setIsLoading(true); + + // Use our custom auth context method to sign in with wallet + const { data, error } = await signInWithWalletConnect(address); + + if (error) { + console.error('Wallet auth error:', error); + toast.error(`Failed to authenticate with wallet: ${error.message}`); + return; + } + + toast.success('Successfully authenticated with wallet'); + router.push('/'); + } catch (error: any) { + console.error('Error authenticating with wallet:', error); + toast.error(`Authentication failed: ${error?.message || 'Unknown error'}`); + } finally { + setIsLoading(false); + } }; - localStorage.setItem('currentUser', JSON.stringify(userData)); - window.location.href = '/'; - } - }, [wallet, router, isLoggedOut]); - - // Helper functions (using localStorage for demo purposes) - const validateEmail = (email: string) => { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email.toLowerCase()); - }; - - const getUsers = () => { - if (typeof window !== 'undefined') { - const usersJSON = localStorage.getItem('users'); - return usersJSON ? JSON.parse(usersJSON) : []; + + authenticateWithWallet(); } - return []; - }; + }, [wallet, router, isLoggedOut, signInWithWalletConnect]); - const isEmailExists = (email: string) => { - const users = getUsers(); - return users.some((user: { email: string }) => user.email === email); - }; - - const validatePasswordForUser = (email: string, password: string) => { - const users = getUsers(); - const user = users.find( - (user: { email: string; password: string }) => user.email === email - ); - return user && user.password === password; - }; - - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setEmailError(''); setPasswordError(''); + setIsLoading(true); - let valid = true; + try { + const { data, error } = await signIn(email, password); - if (!validateEmail(email)) { - setEmailError('Please enter a valid email address.'); - valid = false; - } else if (!isEmailExists(email)) { - setEmailError('Email does not exist.'); - valid = false; - } + if (error) { + if (error.message.includes('email')) { + setEmailError(error.message); + } else if (error.message.includes('password')) { + setPasswordError(error.message); + } else { + toast.error(error.message); + } + return; + } - if (valid && !validatePasswordForUser(email, password)) { - setPasswordError('Incorrect password.'); - valid = false; - } + // Fetch user profile information + const { data: profileData } = await supabase + .from('profiles') + .select('*') + .eq('id', data.user.id) + .single(); - if (valid) { - // Save the current user to localStorage for use in the header - const users = getUsers(); - const loggedInUser = users.find( - (user: { email: string; password: string }) => user.email === email - ); - localStorage.setItem('currentUser', JSON.stringify(loggedInUser)); - window.location.href = "/"; - window.location.reload(); + // Store user data for frontend usage + const userData = { + id: data.user.id, + email: data.user.email, + name: profileData?.display_name || data.user.email?.split('@')[0], + }; + + localStorage.setItem('currentUser', JSON.stringify(userData)); + toast.success('Login successful!'); + router.push('/'); + } catch (error) { + console.error('Login error:', error); + toast.error('An unexpected error occurred. Please try again.'); + } finally { + setIsLoading(false); } }; - const handleWalletConnect = () => { + const handleWalletConnect = async () => { if (!wallet) { - connect(); // Kết nối ví nếu chưa kết nối + connect(); // Connect wallet if not connected } else { - disconnect({ label: wallet.label }); // Ngắt kết nối ví + // Handle wallet disconnect + disconnect({ label: wallet.label }); setAccount(null); - setIsLoggedOut(true); // Đánh dấu đã logout + setIsLoggedOut(true); + + // Sign out of Supabase auth + await supabase.auth.signOut(); + + // Clear local storage localStorage.removeItem('currentUser'); - router.push('/login'); // Chuyển hướng về trang login + router.push('/login'); } }; @@ -274,6 +294,7 @@ function LoginPageContent() { className="w-full px-3 py-2 border border-white rounded-md bg-black text-white" value={email} onChange={(e) => setEmail(e.target.value)} + disabled={isLoading} /> {emailError && {emailError}}
@@ -291,11 +312,13 @@ function LoginPageContent() { className="w-full px-3 py-2 border border-white rounded-md bg-black text-white pr-10" value={password} onChange={(e) => setPassword(e.target.value)} + disabled={isLoading} /> +
) -} \ No newline at end of file +} diff --git a/app/search/TransactionContent.tsx b/app/search/TransactionContent.tsx index 47e6f6f..499b8a5 100644 --- a/app/search/TransactionContent.tsx +++ b/app/search/TransactionContent.tsx @@ -41,4 +41,4 @@ export default function Transactions() {
) -} \ No newline at end of file +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 6e593a7..7bb00c3 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -1,9 +1,12 @@ + 'use client'; import Link from 'next/link'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import ParticlesBackground from '@/components/ParticlesBackground'; +import { toast } from 'sonner'; +import { supabase } from '@/src/integrations/supabase/client'; export default function SignupPage() { const router = useRouter(); @@ -19,36 +22,17 @@ export default function SignupPage() { const [confirmPasswordError, setConfirmPasswordError] = useState(''); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); - - - // Helper functions: using localStorage to store users (as in your original code) + // Helper function for email validation const validateEmail = (email: string) => { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email.toLowerCase()); }; - const getUsers = () => { - if (typeof window !== 'undefined') { - const usersJSON = localStorage.getItem('users'); - return usersJSON ? JSON.parse(usersJSON) : []; - } - return []; - }; - - const isEmailExists = (email: string) => { - const users = getUsers(); - return users.some((user: { email: string }) => user.email === email); - }; - - const saveUser = (user: { name: string; email: string; password: string }) => { - const users = getUsers(); - users.push(user); - localStorage.setItem('users', JSON.stringify(users)); - }; - - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + // Reset error messages setNameError(''); setEmailError(''); @@ -61,37 +45,62 @@ export default function SignupPage() { setNameError('Please enter your full name.'); valid = false; } + if (!validateEmail(email)) { setEmailError('Please enter a valid email address.'); valid = false; - } else if (isEmailExists(email)) { - setEmailError('Email already exists.'); - valid = false; } + if (password.length < 8) { setPasswordError('Password must be at least 8 characters long.'); valid = false; } + if (password !== confirmPassword) { setConfirmPasswordError('Passwords do not match.'); valid = false; } if (valid) { - const newUser = { - name: name.trim(), - email: email.trim(), - password: password, - }; - saveUser(newUser); - alert('Sign up successful!'); - router.push('/login'); // or redirect to login if you prefer + setIsLoading(true); + + try { + // Register the user with Supabase Auth + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + full_name: name.trim(), + }, + }, + }); + + if (error) { + if (error.message.includes('email')) { + setEmailError(error.message); + } else if (error.message.includes('password')) { + setPasswordError(error.message); + } else { + toast.error(error.message); + } + return; + } + + toast.success('Sign up successful! Please check your email for verification.'); + router.push('/login'); + } catch (error) { + console.error('Signup error:', error); + toast.error('An unexpected error occurred. Please try again.'); + } finally { + setIsLoading(false); + } } }; return ( <> -
+
{/* Signup Form */}
@@ -120,6 +129,7 @@ export default function SignupPage() { className="w-full px-3 py-2 border border-white bg-black text-white rounded-md" value={name} onChange={(e) => setName(e.target.value)} + disabled={isLoading} /> {nameError && {nameError}}
@@ -135,6 +145,7 @@ export default function SignupPage() { className="w-full px-3 py-2 border border-white bg-black text-white rounded-md" value={email} onChange={(e) => setEmail(e.target.value)} + disabled={isLoading} /> {emailError && {emailError}}
@@ -150,11 +161,13 @@ export default function SignupPage() { className="w-full px-3 py-2 border border-white bg-black text-white rounded-md pr-10" value={password} onChange={(e) => setPassword(e.target.value)} + disabled={isLoading} /> -
)}
@@ -298,16 +345,10 @@ const Header = () => { -
) : ( @@ -325,4 +366,4 @@ const Header = () => { ); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/components/ParticlesBackground.tsx b/components/ParticlesBackground.tsx index 53ad0a1..9eaee0d 100644 --- a/components/ParticlesBackground.tsx +++ b/components/ParticlesBackground.tsx @@ -114,4 +114,4 @@ const ParticlesBackground = () => { ); }; -export default ParticlesBackground; +export default ParticlesBackground; \ No newline at end of file diff --git a/components/table/ClientContent.tsx b/components/table/ClientContent.tsx index 4ef9631..c194ad7 100644 --- a/components/table/ClientContent.tsx +++ b/components/table/ClientContent.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoins } from "@/lib/api/coinApi"; - import HeroSection from "@/components/table/HeroSection"; import CoinTable from "@/components/table/CoinTable"; import CoinCard from "./CoinCard"; diff --git a/components/table/CoinCard.tsx b/components/table/CoinCard.tsx index 0ba0b3a..568dde4 100644 --- a/components/table/CoinCard.tsx +++ b/components/table/CoinCard.tsx @@ -108,6 +108,4 @@ const CoinCard: React.FC = ({ coin, onCardClick }) => { ); }; - export default CoinCard; - diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx deleted file mode 100644 index ffaf6b2..0000000 --- a/components/transactions/NetworkStats.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client' - -import { useState, useEffect } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Clock, Loader2, Gauge, Calculator } from "lucide-react" -import { toast } from "@/components/ui/use-toast" -import NetworkTransactionTable from '@/components/transactions/NetworkTransactionTable'; - -interface Stats { - transactions24h: number; - pendingTransactions: number; - networkFee: number; - avgGasFee: number; - totalTransactionAmount: number; -} - -const initialStats: Stats = { - transactions24h: 0, - pendingTransactions: 0, - networkFee: 0, - avgGasFee: 0, - totalTransactionAmount: 0, -}; - -export default function NetworkStats() { - const [stats, setStats] = useState(initialStats); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [updateKey, setUpdateKey] = useState(0); // Used to trigger table updates - - const fetchNetworkStats = async () => { - try { - setLoading(true); - setError(null); - - // Batch API calls together - const [gasResponse, blockResponse] = await Promise.all([ - fetch('/api/etherscan?module=gastracker&action=gasoracle'), - fetch('/api/etherscan?module=proxy&action=eth_blockNumber') - ]); - - // Handle gas price data - if (!gasResponse.ok) throw new Error('Failed to fetch gas prices'); - const gasData = await gasResponse.json(); - - // Handle block number data - if (!blockResponse.ok) throw new Error('Failed to fetch latest block'); - const blockData = await blockResponse.json(); - - // Process gas data - if (gasData.status === "1") { - setStats(prev => ({ - ...prev, - networkFee: parseFloat(gasData.result.SafeGasPrice), - avgGasFee: parseFloat(gasData.result.ProposeGasPrice) - })); - } - - // Process block data - const latestBlock = parseInt(blockData.result, 16); - const blocksIn24h = Math.floor(86400 / 15); // Approximate blocks in 24h - - // Fetch transaction count with delay to respect rate limit - await new Promise(resolve => setTimeout(resolve, 200)); - const txCountResponse = await fetch( - `/api/etherscan?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}` - ); - - if (!txCountResponse.ok) throw new Error('Failed to fetch transaction count'); - const txCountData = await txCountResponse.json(); - const txCount = parseInt(txCountData.result, 16); - - // Update stats - setStats(prev => ({ - ...prev, - transactions24h: txCount * blocksIn24h, - pendingTransactions: Math.floor(Math.random() * 100) + 50 // Temporary mock data for pending transactions - })); - - // Trigger table update - setUpdateKey(prev => prev + 1); - } catch (error) { - console.error('Error fetching network stats:', error); - setError('Failed to fetch network stats'); - toast({ - title: "Error", - description: "Failed to fetch network stats. Please try again later.", - variant: "destructive", - }); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchNetworkStats(); - const interval = setInterval(fetchNetworkStats, 15000); // Update every 15 seconds - return () => clearInterval(interval); - }, []); - - return ( -
-
-
- - -
- - Transactions (24h) -
-
- -

- {loading ? ( - - ) : ( - stats.transactions24h.toLocaleString() - )} -

-
-
- - - -
- - Pending Txns -
-
- -

- {loading ? ( - - ) : ( - stats.pendingTransactions.toLocaleString() - )} -

-
-
- - - -
- - Network Fee -
-
- -

- {loading ? ( - - ) : ( - `${stats.networkFee.toFixed(2)} Gwei` - )} -

-
-
- - - -
- - AVG Gas Fee -
-
- -

- {loading ? ( - - ) : ( - `${stats.avgGasFee.toFixed(2)} Gwei` - )} -

-
-
-
- - {/* Transaction Table */} -
- -
-
-
- ); -} \ No newline at end of file diff --git a/components/transactions/NetworkTransactionTable.tsx b/components/transactions/NetworkTransactionTable.tsx deleted file mode 100644 index 224f65a..0000000 --- a/components/transactions/NetworkTransactionTable.tsx +++ /dev/null @@ -1,278 +0,0 @@ -'use client' - -import { useState, useEffect } from 'react' -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Button } from "@/components/ui/button" -import Link from 'next/link' -import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' -import { toast } from "@/components/ui/use-toast" -import { ethers } from 'ethers'; - -interface Transaction { - hash: string; - method: string; - block: string; - age: string; - from: string; - to: string; - amount: string; - fee: string; - timestamp: number; -} - -export default function NetworkTransactionTable() { - const [transactions, setTransactions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [isMobile, setIsMobile] = useState(false); - - interface MethodSignatures { - [key: string]: string; - } - - const knownMethods: MethodSignatures = { - '0xa9059cbb': 'Transfer', - '0x23b872dd': 'TransferFrom', - '0x095ea7b3': 'Approve', - '0x70a08231': 'BalanceOf', - '0x18160ddd': 'TotalSupply', - '0x313ce567': 'Decimals', - '0x06fdde03': 'Name', - '0x95d89b41': 'Symbol', - '0xd0e30db0': 'Deposit', - '0x2e1a7d4d': 'Withdraw', - '0x3593564c': 'Execute', - '0x4a25d94a': 'SwapExactTokensForTokens', - '0x7ff36ab5': 'SwapExactETHForTokens', - '0x791ac947': 'SwapExactTokensForETH', - '0xfb3bdb41': 'SwapETHForExactTokens', - '0x5c11d795': 'SwapTokensForExactTokens', - '0xb6f9de95': 'Claim', - '0x6a627842': 'Mint', - '0xa0712d68': 'Mint', - }; - - const getTransactionMethod = (input: string): string => { - if (input === '0x') return 'Transfer'; - const functionSelector = input.slice(0, 10).toLowerCase(); - return knownMethods[functionSelector] || 'Swap'; - }; - - const getRelativeTime = (timestamp: number) => { - const now = Date.now(); - const diff = now - timestamp * 1000; - if (diff < 0) return "Just now"; - const seconds = Math.floor(diff / 1000); - if (seconds < 60) return `${seconds} secs ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} mins ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours} hrs ago`; - const days = Math.floor(hours / 24); - return `${days} days ago`; - }; - - const truncateAddress = (address: string) => { - return `${address.slice(0, 6)}...${address.slice(-4)}`; - }; - - const fetchLatestTransactions = async () => { - try { - setIsLoading(true); - setError(null); - - const latestBlockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber'); - if (!latestBlockResponse.ok) throw new Error('Failed to fetch latest block'); - const latestBlockData = await latestBlockResponse.json(); - - const response = await fetch( - `/api/etherscan?module=proxy&action=eth_getBlockByNumber&tag=${latestBlockData.result}&boolean=true` - ); - if (!response.ok) throw new Error('Failed to fetch block transactions'); - const data = await response.json(); - - if (data.result && data.result.transactions) { - const formattedTransactions = await Promise.all( - data.result.transactions.slice(0, 50).map(async (tx: any) => { - const timestamp = parseInt(data.result.timestamp, 16); - return { - hash: tx.hash, - method: getTransactionMethod(tx.input), - block: parseInt(tx.blockNumber, 16).toString(), - age: getRelativeTime(timestamp), - from: tx.from, - to: tx.to || 'Contract Creation', - amount: ethers.utils.formatEther(tx.value) + ' ETH', - fee: ethers.utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), - timestamp: timestamp - }; - }) - ); - setTransactions(formattedTransactions); - } - } catch (error) { - console.error('Error fetching transactions:', error); - setError('Failed to fetch transactions'); - toast({ - title: "Error", - description: "Failed to fetch latest transactions. Please try again later.", - variant: "destructive", - }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - fetchLatestTransactions(); - const interval = setInterval(fetchLatestTransactions, 15000); - return () => clearInterval(interval); - }, []); - - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - toast({ - title: "Copied!", - description: "Address copied to clipboard", - }); - } catch (err) { - toast({ - title: "Failed to copy", - description: "Please try again", - variant: "destructive", - }); - } - }; - - if (error) { - return ( -
- {error} -
- ); - } - - return ( -
- - - - - Txn Hash - Method - Block - Age - From - To - Value - Txn Fee - - - - {isLoading ? ( - - -
-
- Loading transactions... -
-
-
- ) : transactions.length === 0 ? ( - - - No transactions found - - - ) : ( - transactions.map((tx, index) => ( - - -
- -
-
- -
- - - {truncateAddress(tx.hash)} - - - -
-
- - - {tx.method} - - - - - - {tx.block} - - - - {tx.age} - -
- - - {truncateAddress(tx.from)} - - - -
-
- -
- - - {truncateAddress(tx.to)} - - - -
-
- {tx.amount} - {tx.fee} -
- )) - )} -
-
-
- ); -} \ No newline at end of file diff --git a/components/transactions/RevenueGraph.tsx b/components/transactions/RevenueGraph.tsx deleted file mode 100644 index 5093401..0000000 --- a/components/transactions/RevenueGraph.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; - -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; - -const data = [ - { month: 'Jan', revenue2024: 15, revenue2023: -15 }, - { month: 'Feb', revenue2024: 5, revenue2023: -18 }, - { month: 'Mar', revenue2024: 12, revenue2023: -10 }, - { month: 'Apr', revenue2024: 25, revenue2023: -15 }, - { month: 'May', revenue2024: 15, revenue2023: -5 }, - { month: 'Jun', revenue2024: 10, revenue2023: -17 }, - { month: 'Jul', revenue2024: 7, revenue2023: -15 }, - { month: 'Aug', revenue2024: 15, revenue2023: -5 }, - { month: 'Sep', revenue2024: 10, revenue2023: -17 }, - { month: 'Oct', revenue2024: 7, revenue2023: -15 }, - { month: 'Nov', revenue2024: 15, revenue2023: -5 }, - { month: 'Dec', revenue2024: 20, revenue2023: -17 }, -]; - -export default function RevenueGraph() { - return ( - - -
- Total Revenue -
-
-
- 2024 -
-
-
- 2023 -
-
-
-
- -
- - - - `${value}`} - /> - - - - - -
-
-
- ); -} \ No newline at end of file diff --git a/components/transactions/TransactionExplorer.tsx b/components/transactions/TransactionExplorer.tsx deleted file mode 100644 index 988979c..0000000 --- a/components/transactions/TransactionExplorer.tsx +++ /dev/null @@ -1,500 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback } from 'react' -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Button } from "@/components/ui/button" -import Link from 'next/link' -import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' -import { toast } from "@/components/ui/use-toast" -import { utils } from 'ethers' - -interface Stats { - transactions24h: number; - pendingTransactions: number; - networkFee: number; - avgGasFee: number; - totalTransactionAmount: number; -} - -// Initial state -const initialStats: Stats = { - transactions24h: 0, - pendingTransactions: 0, - networkFee: 0, - avgGasFee: 0, - totalTransactionAmount: 0, -}; - -export default function TransactionExplorer() { - // State variables - const [transactions, setTransactions] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [selectedMethod, setSelectedMethod] = useState(null); - const [totalPages] = useState(5000); - const [isMobile, setIsMobile] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - // Etherscan API configuration - const ETHERSCAN_API_KEY = '6U137E3DGFMCCBQA8E3CAR1P1UW7EV8A6S'; - - interface MethodSignatures { - [key: string]: string; - } - - const knownMethods: MethodSignatures = { - '0xa9059cbb': 'Transfer', - '0x23b872dd': 'TransferFrom', - '0x095ea7b3': 'Approve', - '0x70a08231': 'BalanceOf', - '0x18160ddd': 'TotalSupply', - '0x313ce567': 'Decimals', - '0x06fdde03': 'Name', - '0x95d89b41': 'Symbol', - '0xd0e30db0': 'Deposit', - '0x2e1a7d4d': 'Withdraw', - '0x3593564c': 'Execute', - '0x4a25d94a': 'SwapExactTokensForTokens', - '0x7ff36ab5': 'SwapExactETHForTokens', - '0x791ac947': 'SwapExactTokensForETH', - '0xfb3bdb41': 'SwapETHForExactTokens', - '0x5c11d795': 'SwapTokensForExactTokens', - '0xb6f9de95': 'Claim', - '0x6a627842': 'Mint', - '0xa0712d68': 'Mint', - }; - - const getTransactionMethod = (input: string): string => { - if (input === '0x') return 'Transfer'; - - const functionSelector = input.slice(0, 10).toLowerCase(); - - if (knownMethods[functionSelector]) { - return knownMethods[functionSelector]; - } - - return 'Swap'; - }; - - // Function to get relative time - const getRelativeTime = (timestamp: number) => { - const now = Date.now(); - const diff = now - timestamp * 1000; - - // Ensure diff is not negative - if (diff < 0) return "Just now"; - - const seconds = Math.floor(diff / 1000); - - if (seconds < 60) return `${seconds} secs ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} mins ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours} hrs ago`; - const days = Math.floor(hours / 24); - return `${days} days ago`; - }; - - // Function to truncate addresses - const truncateAddress = (address: string) => { - return `${address.slice(0, 6)}...${address.slice(-4)}`; - }; - - // Fetch latest blocks and their transactions - const fetchLatestTransactions = useCallback(async () => { - try { - setIsLoading(true); - - // First, get the latest block number - const blockNumberResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` - ); - - if (!blockNumberResponse.ok) { - throw new Error('Failed to fetch latest block number'); - } - - const blockNumberData = await blockNumberResponse.json(); - if (blockNumberData.error) { - throw new Error(blockNumberData.error.message); - } - - const latestBlock = parseInt(blockNumberData.result, 16); - - // Then, get the transactions from the latest block - const response = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true&apikey=${ETHERSCAN_API_KEY}` - ); - - if (!response.ok) { - throw new Error('Failed to fetch block transactions'); - } - - const data = await response.json(); - if (data.error) { - throw new Error(data.error.message); - } - - if (data.result && data.result.transactions) { - const formattedTransactions = await Promise.all( - data.result.transactions.slice(0, 50).map(async (tx: any) => { - const timestamp = parseInt(data.result.timestamp, 16); - return { - hash: tx.hash, - method: getTransactionMethod(tx.input), - block: parseInt(tx.blockNumber, 16).toString(), - age: getRelativeTime(timestamp), - from: tx.from, - to: tx.to || 'Contract Creation', - amount: utils.formatEther(tx.value) + ' ETH', - fee: utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), - timestamp: timestamp - }; - }) - ); - setTransactions(formattedTransactions); - } - } catch (error) { - console.error('Error fetching transactions:', error); - toast({ - title: "Error fetching transactions", - description: error instanceof Error ? error.message : "Failed to fetch latest transactions.", - variant: "destructive", - }); - } finally { - setIsLoading(false); - } - }, [ETHERSCAN_API_KEY]); - - useEffect(() => { - fetchLatestTransactions(); - const interval = setInterval(fetchLatestTransactions, 15000); // Refresh every 15 seconds - return () => clearInterval(interval); - }, [fetchLatestTransactions, currentPage]); - - // Effect to handle responsive design - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - // Utility functions (handleDownload, copyToClipboard, etc.) - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - toast({ - title: "Copied to clipboard", - description: "The text has been copied to your clipboard.", - }); - } catch (err) { - console.error('Failed to copy: ', err); - toast({ - title: "Failed to copy", - description: "An error occurred while copying the text.", - variant: "destructive", - }); - } - }; - - const handleDownload = () => { - const headers = ['Transaction Hash', 'Method', 'Block', 'Age', 'From', 'To', 'Amount', 'Txn Fee']; - const csvContent = [ - headers.join(','), - ...transactions.map(tx => - [ - tx.hash, - tx.method, - tx.block, - formatTimestamp(tx.timestamp), - tx.from, - tx.to, - tx.amount, - tx.fee, - ].join(',') - ) - ].join('\n'); - - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - if (link.download !== undefined) { - const url = URL.createObjectURL(blob); - link.setAttribute('href', url); - link.setAttribute('download', 'ethereum_transactions.csv'); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - }; - - const formatTimestamp = (timestamp: number): string => { - const date = new Date(timestamp * 1000); - const options: Intl.DateTimeFormatOptions = { - timeZone: 'Asia/Bangkok', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }; - return date.toLocaleString('en-GB', options).replace(',', ''); - }; - - const handleMethodClick = (method: string) => { - setSelectedMethod(method === selectedMethod ? null : method); - }; - - const scrollToTop = useCallback(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }, []); - - return ( -
-
- {/* Transaction table header */} -
-
-

Latest {transactions.length} transactions

-

(Auto-updating)

-
- -
- - - -
- Page {currentPage} of {totalPages} -
- - -
-
- - {/* Transaction table */} -
- - - - - Transaction Hash - Method - Block - Age - From - To - Amount - Txn Fee - - - - {isLoading ? ( - - - Loading transactions... - - - ) : ( - transactions.map((tx, index) => ( - - -
- -
-
- -
- - - {truncateAddress(tx.hash)} - - - -
-
- - - - {tx.block} - {tx.age} - -
- - - {truncateAddress(tx.from)} - - - -
-
- -
- - - {truncateAddress(tx.to)} - - - -
-
- {formatAmount(tx.amount)} - {formatFee(tx.fee)} -
- )) - )} -
-
-
- - {/* Pagination controls (bottom) */} -
-
-
- - -
- Page {currentPage} of {totalPages} -
- - -
-
- - {/* Info text */} -

- A transaction is a cryptographically signed instruction that changes the blockchain state. - Block explorers track the details of all transactions in the network. -

- - {/* Back to top button */} - -
-
- ); -} - -// Utility functions -const formatAmount = (amount: string) => { - if (!amount) return '0 ETH'; - const value = parseFloat(amount); - return `${value.toFixed(6)} ETH`; -}; - -const formatFee = (fee: string) => { - if (!fee) return '0'; - const value = parseFloat(fee); - return value.toFixed(6); -}; \ No newline at end of file diff --git a/components/transactions/WalletCharts.tsx b/components/transactions/WalletCharts.tsx deleted file mode 100644 index 7a63dcd..0000000 --- a/components/transactions/WalletCharts.tsx +++ /dev/null @@ -1,296 +0,0 @@ -'use client'; - -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Line, LineChart, BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Area, AreaChart, PieChart, Pie, Cell } from "recharts"; -import { BarChart3, Gauge, Wallet, History, CheckCircle2, Network } from "lucide-react"; - -const transactionTypeData = [ - { week: 'W1', defi: 450, nft: 320, swap: 230 }, - { week: 'W2', defi: 520, nft: 280, swap: 310 }, - { week: 'W3', defi: 710, nft: 420, swap: 380 }, - { week: 'W4', defi: 480, nft: 350, swap: 290 }, - { week: 'W5', defi: 520, nft: 390, swap: 420 }, - { week: 'W6', defi: 630, nft: 450, swap: 380 }, -]; - -const gasUsageData = [ - { name: 'Smart Contracts', usage: 45, percentage: '45%' }, - { name: 'Token Transfers', usage: 30, percentage: '30%' }, - { name: 'NFT Trading', usage: 15, percentage: '15%' }, - { name: 'Other', usage: 10, percentage: '10%' }, -]; - -const miniChartData = { - tokenHoldings: [ - { name: 'ETH', value: 60 }, - { name: 'USDT', value: 25 }, - { name: 'Other', value: 15 }, - ], - walletAge: [ - { date: '1', value: 10 }, - { date: '2', value: 15 }, - { date: '3', value: 12 }, - { date: '4', value: 18 }, - { date: '5', value: 22 }, - { date: '6', value: 20 }, - ], - transactionSuccess: [ - { name: 'Success', value: 85 }, - { name: 'Failed', value: 15 }, - ], - networkInteractions: [ - { name: 'DeFi Protocols', value: 40 }, - { name: 'DEX', value: 30 }, - { name: 'NFT Markets', value: 20 }, - { name: 'Others', value: 10 }, - ], -}; - -const COLORS = ['#F5B056', '#a855f7', '#22c55e', '#666']; - -export default function WalletCharts() { - const tooltipStyle = { - backgroundColor: '#1f2937', - border: 'none', - borderRadius: '8px', - color: '#fff' - }; - - const tooltipFormatter = (value: number, name: string) => { - return [ - `${value}%`, - `${name}`, - ]; - }; - - return ( -
-
- {/* Transaction Types Overview */} - - -
-
- - Transaction Types -
-
-
-
- DeFi -
-
-
- NFT -
-
-
- Swap -
-
-
-
- -
- - - - - - - - - - -
-
-
- - {/* Gas Usage Distribution */} - - -
-
- - Gas Usage Distribution -
-
-
- -
- {gasUsageData.map((item, index) => ( -
-
- {item.name} - {item.percentage} -
-
-
-
-
- ))} -
-
-
-
- -
- {/* Token Distribution */} - - -
- - Token Distribution -
-
- -
- - - - {miniChartData.tokenHoldings.map((entry, index) => ( - - ))} - - - - -
-
-
- - {/* Wallet Age Activity */} - - -
- - Wallet Age Activity -
-
- -
- - - - - - -
-
-
- - {/* Transaction Success Rate */} - - -
-
- - Success Rate -
-
85%
-
-
- -
- - - - - - - - - -
-
-
- - {/* Network Interactions */} - - -
- - Network Interactions -
-
- -
- - - - {miniChartData.networkInteractions.map((entry, index) => ( - - ))} - - - - -
-
-
-
-
- ); -} \ No newline at end of file diff --git a/components/ui/NetworkStats.tsx b/components/ui/NetworkStats.tsx new file mode 100644 index 0000000..4b8aecb --- /dev/null +++ b/components/ui/NetworkStats.tsx @@ -0,0 +1,177 @@ +'use client' + +import { useState, useEffect} from 'react' +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import axios from 'axios'; +import TransactionTable from '@/components/ui/TransactionTable'; + +interface Stats { + transactions24h: number; + pendingTransactions: number; + networkFee: number; + avgGasFee: number; + totalTransactionAmount: number; // New field for total transaction amount +} + +// Initial state +const initialStats: Stats = { + transactions24h: 0, + pendingTransactions: 0, + networkFee: 0, + avgGasFee: 0, + totalTransactionAmount: 0, // Initialize to 0 +}; + +export default function TransactionExplorer() { + // State variables + const [, setIsMobile] = useState(false); + const [stats, setStats] = useState(initialStats); + const [, setTotalTransactions] = useState(0); + const [, setLoading] = useState(true); + const [, setError] = useState(null); + + + // Etherscan API configuration + const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key + const API_URL = `/api/etherscan?module=proxy&action=eth_blockNumber`; + + // Fetch network statistics + const fetchNetworkStats = async () => { + try { + // Get gas price statistics + const gasResponse = await fetch( + `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` + ); + const gasData = await gasResponse.json(); + + if (gasData.status === "1") { + setStats(prev => ({ + ...prev, + networkFee: parseFloat(gasData.result.SafeGasPrice), + avgGasFee: parseFloat(gasData.result.ProposeGasPrice) + })); + } + + // Get 24h transaction count (approximate) + const blockResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` + ); + const blockData = await blockResponse.json(); + const latestBlock = parseInt(blockData.result, 16); + + // Assuming ~15 second block time, calculate blocks in 24h + const blocksIn24h = Math.floor(86400 / 15); + + // Get transaction count for latest block + const txCountResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` + ); + const txCountData = await txCountResponse.json(); + const txCount = parseInt(txCountData.result, 16); + + setStats(prev => ({ + ...prev, + transactions24h: txCount * blocksIn24h, // Rough estimation + pendingTransactions: txCount // Current block's transaction count as pending + })); + } catch (error) { + console.error('Error fetching network stats:', error); + } + }; + + const fetchTotalTransactions = async () => { + setLoading(true); // Đặt loading thành true trước khi gọi API + try { + const response = await axios.get(API_URL); + const totalTxCount = response.data.result; // Giả định bạn có cách lấy số giao dịch từ API + + setTotalTransactions(Number(totalTxCount)); + } catch (err) { + setError('Lỗi khi lấy dữ liệu từ API'); + } finally { + setLoading(false); + } +}; + +useEffect(() => { + fetchTotalTransactions(); + const interval = setInterval(() => { + fetchTotalTransactions(); + }, 300000); + + return () => clearInterval(interval); +}, []); + + + useEffect(() => { + fetchNetworkStats(); + const interval = setInterval(() => { + fetchNetworkStats(); + }, 30000); // Refresh every 5 minutes + + return () => clearInterval(interval); + }, []); + + + // Effect to handle responsive design + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + + return ( +
+
+ {/* Statistics cards */} +
+ + Transactions (24h) + + +

+ {stats.transactions24h.toLocaleString()} +

+
+
+ + + + Pending Txns + + +

{stats.pendingTransactions.toLocaleString()}

+
+
+ + + + Network Fee + + +

{stats.networkFee.toFixed(2)} Gwei

+
+
+ + + + AVG Gas Fee + + +

{stats.avgGasFee.toFixed(2)} Gwei

+
+
+
+ + +
+
+ ); +} + + + \ No newline at end of file diff --git a/components/ui/TransactionTable.tsx b/components/ui/TransactionTable.tsx index 181dee9..90bce34 100644 --- a/components/ui/TransactionTable.tsx +++ b/components/ui/TransactionTable.tsx @@ -1,205 +1,511 @@ -"use client" +'use client' -import { useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" +import { useState, useEffect, useCallback } from 'react' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Button } from "@/components/ui/button" -import { Loader2 } from "lucide-react" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" - -interface Transaction { - id: string - from: string - to: string - value: string - timestamp: string - type: "transfer" | "swap" | "inflow" | "outflow" +import Link from 'next/link' +import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' +import { toast } from "@/components/ui/use-toast" +import { ethers } from 'ethers'; +import { formatEther } from 'ethers/lib/utils'; + + +interface Stats { + transactions24h: number; + pendingTransactions: number; + networkFee: number; + avgGasFee: number; + totalTransactionAmount: number; // New field for total transaction amount } -export default function TransactionTable() { - const searchParams = useSearchParams() - const address = searchParams.get("address") - const [transactions, setTransactions] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [page, setPage] = useState(1) +// Initial state +const initialStats: Stats = { + transactions24h: 0, + pendingTransactions: 0, + networkFee: 0, + avgGasFee: 0, + totalTransactionAmount: 0, // Initialize to 0 +}; + +export default function TransactionExplorer() { + // State variables + const [transactions, setTransactions] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [selectedMethod, setSelectedMethod] = useState(null); + const [totalPages] = useState(5000); + const [isMobile, setIsMobile] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + + // Etherscan API configuration + const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key + const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; + + interface MethodSignatures { + [key: string]: string; + } + + const knownMethods: MethodSignatures = { + '0xa9059cbb': 'Transfer', + '0x23b872dd': 'TransferFrom', + '0x095ea7b3': 'Approve', + '0x70a08231': 'BalanceOf', + '0x18160ddd': 'TotalSupply', + '0x313ce567': 'Decimals', + '0x06fdde03': 'Name', + '0x95d89b41': 'Symbol', + '0xd0e30db0': 'Deposit', + '0x2e1a7d4d': 'Withdraw', + '0x3593564c': 'Execute', + '0x4a25d94a': 'SwapExactTokensForTokens', + '0x7ff36ab5': 'SwapExactETHForTokens', + '0x791ac947': 'SwapExactTokensForETH', + '0xfb3bdb41': 'SwapETHForExactTokens', + '0x5c11d795': 'SwapTokensForExactTokens', + '0xb6f9de95': 'Claim', + '0x6a627842': 'Mint', + '0xa0712d68': 'Mint', + }; + + const getTransactionMethod = (input: string): string => { + if (input === '0x') return 'Transfer'; + + const functionSelector = input.slice(0, 10).toLowerCase(); + + if (knownMethods[functionSelector]) { + return knownMethods[functionSelector]; + } + + return 'Swap'; + }; + + // Function to get relative time +const getRelativeTime = (timestamp: number) => { + const now = Date.now(); + const diff = now - timestamp * 1000; + + // Ensure diff is not negative + if (diff < 0) return "Just now"; + + const seconds = Math.floor(diff / 1000); + + if (seconds < 60) return `${seconds} secs ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} mins ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hrs ago`; + const days = Math.floor(hours / 24); + return `${days} days ago`; +}; + + // Function to truncate addresses + const truncateAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + + // Fetch latest blocks and their transactions + const fetchLatestTransactions = useCallback(async () => { + if (!ETHERSCAN_API_KEY) { + console.error('Etherscan API key is not set') + return + } + + try { + setIsLoading(true) + const latestBlockResponse = await fetch(API_URL) + const latestBlockData = await latestBlockResponse.json() + const latestBlock = parseInt(latestBlockData.result, 16) + + const response = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true&apikey=${ETHERSCAN_API_KEY}` + ) + const data = await response.json() + + if (data.result && data.result.transactions) { + const formattedTransactions = await Promise.all( + data.result.transactions.slice(0, 50).map(async (tx: any) => { + const timestamp = parseInt(data.result.timestamp, 16) + return { + hash: tx.hash, + method: getTransactionMethod(tx.input), + block: parseInt(tx.blockNumber, 16).toString(), + age: getRelativeTime(timestamp), + from: tx.from, + to: tx.to || 'Contract Creation', + amount: formatEther(tx.value) + ' ETH', + fee: formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), + timestamp: timestamp + } + }) + ) + setTransactions(formattedTransactions) + } + } catch (error) { + console.error('Error fetching transactions:', error) + toast({ + title: "Error fetching transactions", + description: "Failed to fetch latest transactions.", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + }, [toast]) useEffect(() => { - if (address) { - setLoading(true) - setError(null) - fetch(`/api/transactions?address=${address}&page=${page}&offset=20`) - .then((res) => res.json()) - .then((data) => { - if (data.error) { - throw new Error(data.error) - } - // Mock categorization of transactions - const categorizedData = data.map((tx: Transaction) => ({ - ...tx, - type: categorizeTransaction(tx, address), - })) - setTransactions(categorizedData) - }) - .catch((err) => { - console.error("Error fetching transactions:", err) - setError(err.message || "Failed to fetch transactions") - }) - .finally(() => setLoading(false)) + fetchLatestTransactions() + const interval = setInterval(fetchLatestTransactions, 150000) // Refresh every 2.5 minutes + return () => clearInterval(interval) + }, [fetchLatestTransactions]) + + + // Effect to fetch data + useEffect(() => { + fetchLatestTransactions(); + const interval = setInterval(() => { + fetchLatestTransactions(); + }, 150000); // Refresh every 2.5 minutes + + return () => clearInterval(interval); + }, [currentPage]); //Refresh every page changes + + // Effect to handle responsive design + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + + // Utility functions (handleDownload, copyToClipboard, etc.) + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast({ + title: "Copied to clipboard", + description: "The text has been copied to your clipboard.", + }); + } catch (err) { + console.error('Failed to copy: ', err); + toast({ + title: "Failed to copy", + description: "An error occurred while copying the text.", + variant: "destructive", + }); } - }, [address, page]) + }; - const categorizeTransaction = (tx: Transaction, userAddress: string): Transaction["type"] => { - if (tx.from === userAddress && tx.to === userAddress) return "swap" - if (tx.from === userAddress) return "outflow" - if (tx.to === userAddress) return "inflow" - return "transfer" - } + const handleDownload = () => { + const headers = ['Transaction Hash', 'Method', 'Block', 'Age', 'From', 'To', 'Amount', 'Txn Fee']; + const csvContent = [ + headers.join(','), + ...transactions.map(tx => + [ + tx.hash, + tx.method, + tx.block, + formatTimestamp(tx.timestamp), + tx.from, + tx.to, + tx.amount, + tx.fee, + ].join(',') + ) + ].join('\n'); - if (loading) { - return ( - - - - - - ) - } + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + if (link.download !== undefined) { + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', 'ethereum_transactions.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }; - if (error) { - return ( - - Error - {error} - - ) - } + const formatTimestamp = (timestamp: number): string => { + // Create a date object from the timestamp + const date = new Date(timestamp * 1000); // Convert seconds to milliseconds - if (transactions.length === 0) { - return ( - - No transactions found. - - ) - } + // Convert to GMT+7 + const options: Intl.DateTimeFormatOptions = { + timeZone: 'Asia/Bangkok', // GMT+7 timezone + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false // Use 24-hour format + }; + + // Format the date + return date.toLocaleString('en-GB', options).replace(',', ''); // Remove comma for better CSV formatting +}; - const renderTransactionTable = (transactions: Transaction[]) => ( - - - - From - To - Value - Timestamp - - - - {transactions.map((tx) => ( - - - {tx.from.slice(0, 6)}...{tx.from.slice(-4)} - - - {tx.to.slice(0, 6)}...{tx.to.slice(-4)} - - {tx.value} - {new Date(tx.timestamp).toLocaleString()} - - ))} - -
- ) + const handleMethodClick = (method: string) => { + setSelectedMethod(method === selectedMethod ? null : method); + }; + + const scrollToTop = useCallback(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, []); return ( - - - Recent Transactions - - - - - +
+ + + {/* Transaction table header */} +
+
+

Latest {transactions.length} transactions

+

(Auto-updating)

+
+ +
+ + +
+ Page {currentPage} of {totalPages} +
+ + +
+
+ + +{/* Transaction table */} +
+ + + + + Transaction Hash + Method + Block + Age + From + To + Amount + Txn Fee + + + + {isLoading ? ( + + + Loading transactions... + + + ) : ( + transactions.map((tx, index) => ( + + +
+ +
+
+ +
+ + + {truncateAddress(tx.hash)} + + + +
+
+ + + + {tx.block} + {tx.age} + +
+ + + {truncateAddress(tx.from)} + + + +
+
+ +
+ + + {truncateAddress(tx.to)} + + + +
+
+ {formatAmount(tx.amount)} + {formatFee(tx.fee)} +
+ )) + )} +
+
+
+ + {/* Pagination controls (bottom) */} +
+
+
+ +
+ Page {currentPage} of {totalPages} +
+ +
- - - ) -} \ No newline at end of file + + {/* Info text */} +

+ A transaction is a cryptographically signed instruction that changes the blockchain state. + Block explorers track the details of all transactions in the network. +

+ + {/* Back to top button */} + +
+
+ ); +} + +// Utility functions +const formatAmount = (amount: string) => { + if (!amount) return '0 ETH'; + const value = parseFloat(amount); + return `${value.toFixed(6)} ETH`; +}; + +const formatFee = (fee: string) => { + if (!fee) return '0'; + const value = parseFloat(fee); + return value.toFixed(6); +}; + + + + \ No newline at end of file diff --git a/lib/api/coinApi.ts b/lib/api/coinApi.ts index 24f7547..b5cb099 100644 --- a/lib/api/coinApi.ts +++ b/lib/api/coinApi.ts @@ -1,14 +1,32 @@ -// lib/api/coinApi.ts + import { toast } from "sonner"; +import { supabase } from "@/src/integrations/supabase/client"; import { Coin, CoinDetail, CoinHistory } from "@/lib/types"; +const CACHE_EXPIRY = 60 * 1000; // 1 minute cache expiry + export const getCoins = async (page = 1, perPage = 20): Promise => { + const cacheKey = `coins_${page}_${perPage}`; + try { + // Try to get cached data + const { data: cachedData } = await supabase + .from('cached_coins') + .select('data, last_updated') + .eq('id', cacheKey) + .single(); + + // If cache is valid and not expired, return it + if (cachedData && Date.now() - new Date(cachedData.last_updated).getTime() < CACHE_EXPIRY) { + console.log('Returning cached coin data'); + return cachedData.data as Coin[]; + } + + // Fetch fresh data from API + console.log(`Fetching fresh coin data for page ${page}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); - console.log(`Fetching coins for page ${page} with ${perPage} per page`); - const response = await fetch( `https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${perPage}&page=${page}&sparkline=true&price_change_percentage=1h,24h,7d&locale=en`, { @@ -23,12 +41,20 @@ export const getCoins = async (page = 1, perPage = 20): Promise => { clearTimeout(timeoutId); if (!response.ok) { - console.error(`API request failed with status ${response.status}: ${response.statusText}`); throw new Error(`API request failed with status ${response.status}`); } const data = await response.json(); - console.log(`Successfully fetched ${data.length} coins`); + + // Update cache with new data + await supabase + .from('cached_coins') + .upsert({ + id: cacheKey, + data: data, + last_updated: new Date().toISOString() + }); + return data; } catch (error) { console.error("Error fetching coins:", error); @@ -44,11 +70,25 @@ export const getCoinDetail = async (id: string): Promise => { } try { + // Try to get cached data + const { data: cachedData } = await supabase + .from('cached_coin_details') + .select('data, last_updated') + .eq('id', id) + .single(); + + // If cache is valid and not expired, return it + if (cachedData && Date.now() - new Date(cachedData.last_updated).getTime() < CACHE_EXPIRY) { + console.log('Returning cached coin detail'); + return cachedData.data as CoinDetail; + } + + // Fetch fresh data + console.log(`Fetching fresh data for coin: ${id}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); const url = `https://api.coingecko.com/api/v3/coins/${id}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=true`; - console.log(`Fetching data for coin with id: ${id}, URL: ${url}`); const response = await fetch(url, { signal: controller.signal, @@ -62,27 +102,30 @@ export const getCoinDetail = async (id: string): Promise => { clearTimeout(timeoutId); if (!response.ok) { - const errorText = await response.text(); // Lấy chi tiết lỗi từ server - console.error(`API request failed with status ${response.status}: ${response.statusText} - ${errorText}`); - throw new Error(`API request failed with status ${response.status}: ${response.statusText} - ${errorText}`); + throw new Error(`API request failed with status ${response.status}`); } const data = await response.json(); - console.log(`Successfully fetched data for coin: ${data.name}`); + + // Update cache with new data + await supabase + .from('cached_coin_details') + .upsert({ + id: id, + data: data, + last_updated: new Date().toISOString() + }); + return data; } catch (error) { if (error instanceof Error) { - if (error.name === "AbortError") { - console.error(`Fetch aborted for coin ${id} due to timeout`); - throw new Error("Request timed out after 30 seconds"); - } - console.error(`Error fetching coin detail for ${id}: ${error.message}`); + console.error(`Error fetching coin detail for ${id}:`, error); throw error; } - console.error(`Unknown error fetching coin detail for ${id}:`, error); throw new Error("An unknown error occurred while fetching coin data"); } }; + export const getCoinHistory = async ( id: string, days = 7 @@ -112,4 +155,4 @@ export const getCoinHistory = async ( total_volumes: Array.from({ length: 168 }, (_, i) => [Date.now() - (168 - i) * 3600000, 50000000000 + Math.random() * 10000000000]), }; } -}; \ No newline at end of file +}; diff --git a/lib/api/globalApi.ts b/lib/api/globalApi.ts index 666d302..e369b0e 100644 --- a/lib/api/globalApi.ts +++ b/lib/api/globalApi.ts @@ -1,9 +1,27 @@ -// lib/api/globalApi.ts + import { toast } from "sonner"; +import { supabase } from "@/src/integrations/supabase/client"; import { GlobalData } from "@/lib/types"; +const CACHE_EXPIRY = 60 * 1000; // 1 minute cache expiry + export const getGlobalData = async (): Promise => { try { + // Try to get cached data + const { data: cachedData } = await supabase + .from('cached_global_data') + .select('data, last_updated') + .eq('id', 'global') + .single(); + + // If cache is valid and not expired, return it + if (cachedData && Date.now() - new Date(cachedData.last_updated).getTime() < CACHE_EXPIRY) { + console.log('Returning cached global data'); + return cachedData.data as GlobalData; + } + + // Fetch fresh data + console.log('Fetching fresh global data'); const response = await fetch("https://api.coingecko.com/api/v3/global", { method: "GET", headers: { @@ -15,12 +33,24 @@ export const getGlobalData = async (): Promise => { throw new Error(`API request failed with status ${response.status}`); } - const data = await response.json(); - return data.data; + const responseData = await response.json(); + const data = responseData.data; + + // Update cache with new data + await supabase + .from('cached_global_data') + .upsert({ + id: 'global', + data: data, + last_updated: new Date().toISOString() + }); + + return data; } catch (error) { console.error("Error fetching global data:", error); toast.error("Failed to load global market data"); + // Return fallback data if fetch fails return { active_cryptocurrencies: 10000, upcoming_icos: 0, @@ -56,4 +86,4 @@ export const getGlobalData = async (): Promise => { updated_at: Math.floor(Date.now() / 1000), }; } -}; \ No newline at end of file +}; diff --git a/lib/context/AuthContext.tsx b/lib/context/AuthContext.tsx new file mode 100644 index 0000000..c4b9ea8 --- /dev/null +++ b/lib/context/AuthContext.tsx @@ -0,0 +1,297 @@ + +'use client'; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { Session, User } from '@supabase/supabase-js'; +import { supabase } from '@/src/integrations/supabase/client'; + +type AuthContextType = { + user: User | null; + session: Session | null; + isLoading: boolean; + signUp: (email: string, password: string, meta?: any) => Promise; + signIn: (email: string, password: string) => Promise; + signOut: () => Promise; + signInWithGoogle: () => Promise; + signInWithWalletConnect: (address: string) => Promise; + checkSession: () => Promise; +}; + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Check active session and set user + const checkSession = async () => { + setIsLoading(true); + try { + const { data, error } = await supabase.auth.getSession(); + if (error) throw error; + + setSession(data.session); + setUser(data.session?.user || null); + } catch (error) { + console.error('Error checking auth session:', error); + } finally { + setIsLoading(false); + } + }; + + checkSession(); + + // Listen for auth changes + const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => { + setSession(session); + setUser(session?.user || null); + setIsLoading(false); + }); + + return () => { + authListener.subscription.unsubscribe(); + }; + }, []); + + // Sign up a new user + const signUp = async (email: string, password: string, meta?: any) => { + try { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + ...meta, + auth_provider: 'email' + } + } + }); + + if (error) throw error; + return data; + } catch (error) { + console.error('Error signing up:', error); + throw error; + } + }; + + // Sign in a user + const signIn = async (email: string, password: string) => { + try { + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password + }); + + if (error) throw error; + + // Update auth provider in profiles table + if (data.user) { + await supabase + .from('profiles') + .update({ auth_provider: 'email' }) + .eq('id', data.user.id); + } + + return data; + } catch (error) { + console.error('Error signing in:', error); + throw error; + } + }; + + // Sign in with Google + const signInWithGoogle = async () => { + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/`, + queryParams: { + // Pass auth_provider to handle in the callback + access_type: 'offline', + prompt: 'consent', + } + }, + }); + + if (error) throw error; + + // Note: We'll update the auth_provider in profiles after the OAuth callback + // This happens automatically through Supabase's auth state change listener + } catch (error) { + console.error('Error signing in with Google:', error); + throw error; + } + }; + + // Sign in with Wallet Connect - Modified to use a valid email format + const signInWithWalletConnect = async (address: string) => { + try { + if (!address) { + throw new Error('Wallet address is required'); + } + + // For wallet authentication, use a valid email format + // Replace @ in the wallet address with something else to form a valid email + const normalizedAddress = address.toLowerCase(); + const validEmail = `wallet_${normalizedAddress.replace('0x', '')}@cryptopath.com`; + const password = `${normalizedAddress}-secure-pass`; + + console.log('Attempting to authenticate with wallet address:', address); + console.log('Using valid email format:', validEmail); + + // Try to sign in with existing account + try { + const { data: signInData, error: signInError } = await supabase.auth.signInWithPassword({ + email: validEmail, + password + }); + + if (!signInError) { + console.log("Successfully signed in with wallet address"); + + // Update the profile with wallet address if needed + if (signInData.user) { + // Create the shortened display name format + const displayName = `Wallet ${address.slice(0, 6)}...${address.slice(-4)}`; + + await supabase + .from('profiles') + .update({ + wallet_address: address, + auth_provider: 'wallet', + display_name: displayName // Add this line to set the display name + }) + .eq('id', signInData.user.id); + + // Store user data for frontend usage + const userData = { + id: signInData.user.id, + email: validEmail, + name: displayName, // Use the shortened display name + walletAddress: address + }; + + localStorage.setItem('currentUser', JSON.stringify(userData)); + + // Create currentUser object for event + const currentUser = { + id: signInData.user.id, + email: validEmail, + name: displayName + }; + + // Emit an event to notify components about the user change + window.dispatchEvent(new CustomEvent('userUpdated', { detail: currentUser })); + } + + return signInData; + } + + console.log("Sign in failed, creating new user", signInError); + } catch (signInErr) { + console.error("Error during wallet sign in attempt:", signInErr); + // Continue to signup if signin fails + } + + // Create a new user with the wallet address + console.log("Creating new user with wallet"); + const { data: signUpData, error: signUpError } = await supabase.auth.signUp({ + email: validEmail, + password, + options: { + data: { + wallet_address: address, + auth_provider: 'wallet' + } + } + }); + + if (signUpError) { + console.error("Error creating wallet user:", signUpError); + throw signUpError; + } + + console.log("Successfully created wallet user"); + + // Update the profile with wallet address and display name + if (signUpData.user) { + const displayName = `Wallet ${address.slice(0, 6)}...${address.slice(-4)}`; + + await supabase + .from('profiles') + .update({ + wallet_address: address, + auth_provider: 'wallet', + display_name: displayName + }) + .eq('id', signUpData.user.id); + + // Store user data for frontend usage + const userData = { + id: signUpData.user.id, + email: validEmail, + name: displayName, + walletAddress: address + }; + + localStorage.setItem('currentUser', JSON.stringify(userData)); + } + + return signUpData; + } catch (error) { + console.error('Error signing in with wallet:', error); + throw error; + } + }; + + // Sign out + const signOut = async () => { + try { + const { error } = await supabase.auth.signOut(); + if (error) throw error; + + // Clear any local storage used for authentication + localStorage.removeItem('currentUser'); + } catch (error) { + console.error('Error signing out:', error); + throw error; + } + }; + + // Check if user has active session + const checkSession = async (): Promise => { + try { + const { data } = await supabase.auth.getSession(); + return !!data.session; + } catch (error) { + console.error('Error checking session:', error); + return false; + } + }; + + const value = { + user, + session, + isLoading, + signUp, + signIn, + signOut, + signInWithGoogle, + signInWithWalletConnect, + checkSession + }; + + return {children}; +} + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/lib/types.ts b/lib/types.ts index a20a886..82f7015 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,191 +1,179 @@ // lib/types.ts - - -// Transaction related types -export interface TransactionTableProps { - data?: { - id: string; - from: string; - to: string; - value: string; - timestamp: string; - }[]; -} - -// Cryptocurrency related types export type Coin = { - id: string; - symbol: string; - name: string; - image: string; - current_price: number; - market_cap: number; - market_cap_rank: number; - fully_diluted_valuation: number; - total_volume: number; - high_24h: number; - low_24h: number; - price_change_24h: number; - price_change_percentage_24h: number; - market_cap_change_24h: number; - market_cap_change_percentage_24h: number; - circulating_supply: number; - total_supply: number; - max_supply: number; - ath: number; - ath_change_percentage: number; - ath_date: string; - atlDance: number; - atl_change_percentage: number; - atl_date: string; - roi: { - times: number; - currency: string; - percentage: number; - } | null; - last_updated: string; - sparkline_in_7d: { - price: number[]; - }; - price_change_percentage_1h_in_currency: number; - price_change_percentage_24h_in_currency: number; - price_change_percentage_7d_in_currency: number; -}; - -export type CoinDetail = { - id: string; - symbol: string; - name: string; - description: { - en: string; - }; - image: { - thumb: string; - small: string; - large: string; - }; - market_cap_rank: number; - links: { - homepage: string[]; - blockchain_site: string[]; - official_forum_url: string[]; - chat_url: string[]; - announcement_url: string[]; - twitter_screen_name: string; - facebook_username: string; - bitcointalk_thread_identifier: number | null; - telegram_channel_identifier: string; - subreddit_url: string; - repos_url: { - github: string[]; - bitbucket: string[]; - }; - }; - market_data: { - current_price: { - usd: number; - }; - market_cap: { - usd: number; - }; + id: string; + symbol: string; + name: string; + image: string; + current_price: number; + market_cap: number; market_cap_rank: number; - fully_diluted_valuation: { - usd: number; - }; - total_volume: { - usd: number; - }; - high_24h: { - usd: number; - }; - low_24h: { - usd: number; - }; + fully_diluted_valuation: number; + total_volume: number; + high_24h: number; + low_24h: number; price_change_24h: number; price_change_percentage_24h: number; - price_change_percentage_7d: number; - price_change_percentage_14d: number; - price_change_percentage_30d: number; - price_change_percentage_60d: number; - price_change_percentage_200d: number; - price_change_percentage_1y: number; market_cap_change_24h: number; market_cap_change_percentage_24h: number; - price_change_24h_in_currency: { - usd: number; - }; - price_change_percentage_1h_in_currency: { - usd: number; - }; - price_change_percentage_24h_in_currency: { - usd: number; - }; - price_change_percentage_7d_in_currency: { - usd: number; - }; - price_change_percentage_14d_in_currency: { - usd: number; - }; - price_change_percentage_30d_in_currency: { - usd: number; - }; - price_change_percentage_60d_in_currency: { - usd: number; - }; - price_change_percentage_200d_in_currency: { - usd: number; - }; - price_change_percentage_1y_in_currency: { - usd: number; - }; - max_supply: number; circulating_supply: number; total_supply: number; - sparkline_7d: { + max_supply: number; + ath: number; + ath_change_percentage: number; + ath_date: string; + atlDance: number; + atl_change_percentage: number; + atl_date: string; + roi: { + times: number; + currency: string; + percentage: number; + } | null; + last_updated: string; + sparkline_in_7d: { price: number[]; }; - ath: { - usd: number; + price_change_percentage_1h_in_currency: number; + price_change_percentage_24h_in_currency: number; + price_change_percentage_7d_in_currency: number; + }; + + export type CoinDetail = { + id: string; + symbol: string; + name: string; + description: { + en: string; }; - ath_change_percentage: { - usd: number; + image: { + thumb: string; + small: string; + large: string; }; - ath_date: { - usd: string; + market_cap_rank: number; + links: { + homepage: string[]; + blockchain_site: string[]; + official_forum_url: string[]; + chat_url: string[]; + announcement_url: string[]; + twitter_screen_name: string; + facebook_username: string; + bitcointalk_thread_identifier: number | null; + telegram_channel_identifier: string; + subreddit_url: string; + repos_url: { + github: string[]; + bitbucket: string[]; + }; + }; + market_data: { + current_price: { + usd: number; + }; + market_cap: { + usd: number; + }; + market_cap_rank: number; + fully_diluted_valuation: { + usd: number; + }; + total_volume: { + usd: number; + }; + high_24h: { + usd: number; + }; + low_24h: { + usd: number; + }; + price_change_24h: number; + price_change_percentage_24h: number; + price_change_percentage_7d: number; + price_change_percentage_14d: number; + price_change_percentage_30d: number; + price_change_percentage_60d: number; + price_change_percentage_200d: number; + price_change_percentage_1y: number; + market_cap_change_24h: number; + market_cap_change_percentage_24h: number; + price_change_24h_in_currency: { + usd: number; + }; + price_change_percentage_1h_in_currency: { + usd: number; + }; + price_change_percentage_24h_in_currency: { + usd: number; + }; + price_change_percentage_7d_in_currency: { + usd: number; + }; + price_change_percentage_14d_in_currency: { + usd: number; + }; + price_change_percentage_30d_in_currency: { + usd: number; + }; + price_change_percentage_60d_in_currency: { + usd: number; + }; + price_change_percentage_200d_in_currency: { + usd: number; + }; + price_change_percentage_1y_in_currency: { + usd: number; + }; + max_supply: number; + circulating_supply: number; + total_supply: number; + sparkline_7d: { + price: number[]; + }; + ath: { + usd: number; + }; + ath_change_percentage: { + usd: number; + }; + ath_date: { + usd: string; + }; + atl: { + usd: number; + }; + atl_change_percentage: { + usd: number; + }; + atl_date: { + usd: string; + }; }; - atl: { - usd: number; +}; + + + + export type GlobalData = { + active_cryptocurrencies: number; + upcoming_icos: number; + ongoing_icos: number; + ended_icos: number; + markets: number; + total_market_cap: { + [key: string]: number; }; - atl_change_percentage: { - usd: number; + total_volume: { + [key: string]: number; }; - atl_date: { - usd: string; + market_cap_percentage: { + [key: string]: number; }; + market_cap_change_percentage_24h_usd: number; + updated_at: number; }; -}; - -export type GlobalData = { - active_cryptocurrencies: number; - upcoming_icos: number; - ongoing_icos: number; - ended_icos: number; - markets: number; - total_market_cap: { - [key: string]: number; - }; - total_volume: { - [key: string]: number; - }; - market_cap_percentage: { - [key: string]: number; - }; - market_cap_change_percentage_24h_usd: number; - updated_at: number; -}; - -export type CoinHistory = { - prices: [number, number][]; - market_caps: [number, number][]; - total_volumes: [number, number][]; -}; \ No newline at end of file + + export type CoinHistory = { + prices: [number, number][]; + market_caps: [number, number][]; + total_volumes: [number, number][]; + }; \ No newline at end of file diff --git a/next.config.js b/next.config.js deleted file mode 100644 index c0ee7f5..0000000 --- a/next.config.js +++ /dev/null @@ -1,31 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - webpack: (config, { isServer }) => { - if (!isServer) { - // Client-side polyfills - config.resolve.fallback = { - ...config.resolve.fallback, - crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify'), - path: require.resolve('path-browserify'), - buffer: require.resolve('buffer/'), - fs: false, - net: false, - tls: false, - http: false, - https: false, - zlib: false - } - } - - // Ignore native module build errors - config.resolve.alias = { - ...config.resolve.alias, - './build/Release/ecdh': false, - } - - return config - }, -} - -module.exports = nextConfig \ No newline at end of file diff --git a/next.config.js.backup b/next.config.js.backup deleted file mode 100644 index c0ee7f5..0000000 --- a/next.config.js.backup +++ /dev/null @@ -1,31 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - webpack: (config, { isServer }) => { - if (!isServer) { - // Client-side polyfills - config.resolve.fallback = { - ...config.resolve.fallback, - crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify'), - path: require.resolve('path-browserify'), - buffer: require.resolve('buffer/'), - fs: false, - net: false, - tls: false, - http: false, - https: false, - zlib: false - } - } - - // Ignore native module build errors - config.resolve.alias = { - ...config.resolve.alias, - './build/Release/ecdh': false, - } - - return config - }, -} - -module.exports = nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c764ae3..f862e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,8 +34,8 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", + "@supabase/supabase-js": "^2.49.1", "@tanstack/react-query": "^5.67.1", - "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -52,12 +52,9 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", - "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", - "crypto-browserify": "^3.12.1", - "dotenv": "^16.4.7", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -97,9 +94,7 @@ "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", - "path-browserify": "^1.0.1", "postcss": "^8", - "stream-browserify": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -311,6 +306,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -325,14 +334,78 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -353,6 +426,43 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -504,13 +614,27 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helpers": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -630,16 +754,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -657,9 +781,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -5465,6 +5589,89 @@ "typescript": ">=5" } }, + "node_modules/@supabase/auth-js": { + "version": "2.68.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz", + "integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz", + "integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/realtime-js/node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.49.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz", + "integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.68.0", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.2", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -5506,34 +5713,6 @@ "react": "^18 || ^19" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz", - "integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.2", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz", - "integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@trezor/analytics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.3.0.tgz", @@ -6127,11 +6306,17 @@ "@types/node": "*" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6141,7 +6326,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -8658,23 +8843,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "license": "MIT" - }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -9057,119 +9225,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "license": "MIT", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "license": "MIT", - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "license": "ISC", - "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/browserify-sign/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -9672,6 +9727,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT", + "peer": true + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -9693,12 +9755,6 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -9711,22 +9767,6 @@ "node": ">=0.8" } }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "license": "MIT" - }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -9786,45 +9826,6 @@ "uncrypto": "^0.1.3" } }, - "node_modules/crypto-browserify": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "license": "MIT", - "dependencies": { - "browserify-cipher": "^1.0.1", - "browserify-sign": "^4.2.3", - "create-ecdh": "^4.0.4", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "diffie-hellman": "^5.0.3", - "hash-base": "~3.0.4", - "inherits": "^2.0.4", - "pbkdf2": "^3.1.2", - "public-encrypt": "^4.0.3", - "randombytes": "^2.1.0", - "randomfill": "^1.0.4" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/crypto-browserify/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/crypto-es": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/crypto-es/-/crypto-es-1.2.7.tgz", @@ -10330,16 +10331,6 @@ "node": ">=0.4.0" } }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/destr": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", @@ -10374,23 +10365,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "license": "MIT" - }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -10431,18 +10405,6 @@ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, - "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -10570,15 +10532,13 @@ "node": ">=4.0.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.114", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", - "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "version": "1.5.115", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.115.tgz", + "integrity": "sha512-MN1nahVHAQMOz6dz6bNZ7apgqc9InZy7Ja4DBEVCTdeiUcegbyOYE9bi/f2Z/z6ZxLi0RxLpyJ3EGe+4h3w73A==", "license": "ISC", "peer": true }, - "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10936,6 +10896,16 @@ "@esbuild/win32-x64": "0.19.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12656,6 +12626,13 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", @@ -12929,6 +12906,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -14690,25 +14677,6 @@ "node": ">=8.6" } }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "license": "MIT" - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -15047,6 +15015,13 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT", + "peer": true + }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -15451,36 +15426,6 @@ "node": ">=6" } }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "license": "ISC", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-asn1/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse-headers": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", @@ -15493,13 +15438,6 @@ "integrity": "sha512-8e0JIqkRbMMPlFBnF9f+92hX1s07jdkd3tqB8uHE9L+cwGGjIYjQM7QLgt0FQ5MZp6SFFYYDm/Y48pqK3ZvJOQ==", "license": "MIT" }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -15871,12 +15809,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/process-warning": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", @@ -15936,26 +15868,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -16062,16 +15974,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "license": "MIT", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -18095,7 +17997,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18306,6 +18207,37 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -20089,6 +20021,13 @@ "node": ">=0.10.32" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "peer": true + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", diff --git a/package.json b/package.json index ea23c0f..e82282c 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "@radix-ui/react-tooltip": "^1.1.8", "@safe-global/safe-apps-provider": "^0.18.5", "@safe-global/safe-apps-sdk": "^8.1.0", + "@supabase/supabase-js": "^2.49.1", "@tanstack/react-query": "^5.67.1", - "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -53,12 +53,9 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", - "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", - "crypto-browserify": "^3.12.1", - "dotenv": "^16.4.7", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -98,9 +95,7 @@ "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", - "path-browserify": "^1.0.1", "postcss": "^8", - "stream-browserify": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/src/integrations/supabase/client.ts b/src/integrations/supabase/client.ts new file mode 100644 index 0000000..63d4962 --- /dev/null +++ b/src/integrations/supabase/client.ts @@ -0,0 +1,11 @@ +// This file is automatically generated. Do not edit it directly. +import { createClient } from '@supabase/supabase-js'; +import type { Database } from './types'; + +const SUPABASE_URL = "https://zqasnuenammuztqrtvjf.supabase.co"; +const SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpxYXNudWVuYW1tdXp0cXJ0dmpmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDE3MDM0NjcsImV4cCI6MjA1NzI3OTQ2N30.vsuz-vRDryIWtjEZwYNpOghVLc3ui06OIOH8dBOYgb8"; + +// Import the supabase client like this: +// import { supabase } from "@/integrations/supabase/client"; + +export const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY); \ No newline at end of file diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts new file mode 100644 index 0000000..93719c0 --- /dev/null +++ b/src/integrations/supabase/types.ts @@ -0,0 +1,210 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + public: { + Tables: { + cached_coin_details: { + Row: { + data: Json + id: string + last_updated: string + } + Insert: { + data: Json + id: string + last_updated?: string + } + Update: { + data?: Json + id?: string + last_updated?: string + } + Relationships: [] + } + cached_coins: { + Row: { + data: Json + id: string + last_updated: string + } + Insert: { + data: Json + id: string + last_updated?: string + } + Update: { + data?: Json + id?: string + last_updated?: string + } + Relationships: [] + } + cached_global_data: { + Row: { + data: Json + id: string + last_updated: string + } + Insert: { + data: Json + id: string + last_updated?: string + } + Update: { + data?: Json + id?: string + last_updated?: string + } + Relationships: [] + } + profiles: { + Row: { + auth_provider: string | null + avatar_url: string | null + created_at: string + display_name: string | null + id: string + updated_at: string + username: string | null + wallet_address: string | null + } + Insert: { + auth_provider?: string | null + avatar_url?: string | null + created_at?: string + display_name?: string | null + id: string + updated_at?: string + username?: string | null + wallet_address?: string | null + } + Update: { + auth_provider?: string | null + avatar_url?: string | null + created_at?: string + display_name?: string | null + id?: string + updated_at?: string + username?: string | null + wallet_address?: string | null + } + Relationships: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type PublicSchema = Database[Extract] + +export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & + PublicSchema["Views"]) + ? (PublicSchema["Tables"] & + PublicSchema["Views"])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends + | keyof PublicSchema["Enums"] + | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] + ? PublicSchema["Enums"][PublicEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof PublicSchema["CompositeTypes"] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] + ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..d5c8d62 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1 @@ +project_id = "zqasnuenammuztqrtvjf" \ No newline at end of file From 7a797f6eb528f922fe6fbc7c5b4b19f6342577f6 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:39:28 +0700 Subject: [PATCH 026/107] Update README with NextAuth and WalletConnect configuration details --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index df6990d..28024fe 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ SMTP_PASSWORD=your-password NEO4J_URI=neo4j+s://your-database-uri NEO4J_USERNAME=your-username NEO4J_PASSWORD=your-password +NEXTAUTH_URL=https://cryptopath.vercel.app/ +NEXTAUTH_SECRET=your-secret-key +NEXT_PUBLIC_INFURA_KEY=your-infura-key +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your-walletconnect-projectid ``` ```bash # Start the development server From 56e3e60571729b872775cff3d9f9d2b98a1797dd Mon Sep 17 00:00:00 2001 From: Minh Duy - Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:51:41 +0700 Subject: [PATCH 027/107] Update route.ts --- app/api/etherscan/route.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/api/etherscan/route.ts b/app/api/etherscan/route.ts index 7d8bac3..bdac5ed 100644 --- a/app/api/etherscan/route.ts +++ b/app/api/etherscan/route.ts @@ -1,5 +1,11 @@ import { NextResponse } from "next/server" +// Simple in-memory cache +const cache = new Map(); +const CACHE_DURATION = 5000; // 5 seconds cache +let lastCallTimestamp = 0; +const RATE_LIMIT_WINDOW = 200; // 200ms between calls (5 calls per second) + export async function GET(request: Request) { const { searchParams } = new URL(request.url) const moduleParam = searchParams.get("module") From 371b9beafb1454f5ae4fb1f661fbc9b1f336b979 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:03:51 +0700 Subject: [PATCH 028/107] keep main --- app/api/etherscan/route.ts | 56 ++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/app/api/etherscan/route.ts b/app/api/etherscan/route.ts index bdac5ed..2a46e51 100644 --- a/app/api/etherscan/route.ts +++ b/app/api/etherscan/route.ts @@ -8,15 +8,61 @@ const RATE_LIMIT_WINDOW = 200; // 200ms between calls (5 calls per second) export async function GET(request: Request) { const { searchParams } = new URL(request.url) - const moduleParam = searchParams.get("module") - const action = searchParams.get("action") - const url = `https://api.etherscan.io/api?module=${moduleParam}&action=${action}&apikey=${process.env.ETHERSCAN_API_KEY}` + // Create cache key from the entire URL + const cacheKey = searchParams.toString(); + + // Check cache + const cachedData = cache.get(cacheKey); + if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) { + return NextResponse.json(cachedData.data); + } + + // Rate limiting + const now = Date.now(); + if (now - lastCallTimestamp < RATE_LIMIT_WINDOW) { + await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_WINDOW)); + } + lastCallTimestamp = Date.now(); + + // Build the Etherscan API URL with all parameters + const urlParams = new URLSearchParams() + + // Add all search params to the URL + searchParams.forEach((value, key) => { + urlParams.append(key, value) + }) + + // Always include the API key + urlParams.append('apikey', process.env.ETHERSCAN_API_KEY || '') + + const url = `https://api.etherscan.io/api?${urlParams.toString()}` + try { const response = await fetch(url) + if (!response.ok) { + throw new Error(`Etherscan API responded with status: ${response.status}`) + } const data = await response.json() + + // Check for Etherscan API errors + if (data.status === "0" && data.message === "NOTOK") { + throw new Error(data.result) + } + + // Cache the successful response + cache.set(cacheKey, { data, timestamp: Date.now() }); + return NextResponse.json(data) } catch (error) { - return NextResponse.json({ error: "Failed to fetch from Etherscan" }, { status: 500 }) + console.error('Etherscan API error:', error) + return NextResponse.json( + { + status: "0", + message: "NOTOK", + result: error instanceof Error ? error.message : "Failed to fetch from Etherscan" + }, + { status: 500 } + ) } -} +} \ No newline at end of file From a639384478aa2e3d295030826f379bb912ce3d4b Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:12:31 +0700 Subject: [PATCH 029/107] Refactor login page to use environment variables for Infura and WalletConnect project IDs --- app/login/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index fa09f0b..3bc1345 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -24,11 +24,11 @@ import { useAuth } from '@/lib/context/AuthContext'; const dcent = dcentModule(); -const INFURA_KEY = '7d389678fba04ceb9510b2be4fff5129'; // Replace with your Infura key +const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY; // Replace with your Infura key // Initialize WalletConnect with projectId const walletConnect = walletConnectModule({ - projectId: 'b773e42585868b9b143bb0f1664670f1', // Replace with your WalletConnect project ID + projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, // Replace with your WalletConnect project ID optionalChains: [1, 137] // Optional: specify chains you want to support }); From ffc7f55285a2497281dd4bde3c0ee521548eb52f Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:18:45 +0700 Subject: [PATCH 030/107] Update login page to store only non-sensitive user display information in localStorage --- app/login/page.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index 3bc1345..84682a9 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -229,14 +229,15 @@ function LoginPageContent() { .eq('id', data.user.id) .single(); - // Store user data for frontend usage - const userData = { - id: data.user.id, - email: data.user.email, + // Store only non-sensitive display information in localStorage + // Rely on Supabase session for authentication and sensitive data + const publicUserData = { name: profileData?.display_name || data.user.email?.split('@')[0], + isLoggedIn: true, + // Avoid storing email, ID or other sensitive information }; - localStorage.setItem('currentUser', JSON.stringify(userData)); + localStorage.setItem('userDisplayInfo', JSON.stringify(publicUserData)); toast.success('Login successful!'); router.push('/'); } catch (error) { From b8a23a32f4239a6c3a8cb7cb7616a6037fb098ad Mon Sep 17 00:00:00 2001 From: Minh Duy - Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:36:20 +0700 Subject: [PATCH 031/107] Potential fix for code scanning alert no. 5: Clear text storage of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- app/login/page.tsx | 8 +++++++- package.json | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index 84682a9..1e58ba2 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import CryptoJS from 'crypto-js'; import Link from 'next/link'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; @@ -24,6 +25,10 @@ import { useAuth } from '@/lib/context/AuthContext'; const dcent = dcentModule(); +const encryptData = (data) => { + const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), 'secret-key').toString(); + return ciphertext; +}; const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY; // Replace with your Infura key // Initialize WalletConnect with projectId @@ -237,7 +242,8 @@ function LoginPageContent() { // Avoid storing email, ID or other sensitive information }; - localStorage.setItem('userDisplayInfo', JSON.stringify(publicUserData)); + const encryptedUserData = encryptData(publicUserData); + localStorage.setItem('userDisplayInfo', encryptedUserData); toast.success('Login successful!'); router.push('/'); } catch (error) { diff --git a/package.json b/package.json index e82282c..c1e496b 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,8 @@ "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "test": "file:", - "vaul": "^1.1.2" + "vaul": "^1.1.2", + "crypto-js": "^4.2.0" }, "devDependencies": { "@eslint/eslintrc": "^3", From 72e0d4a9880fd58fca019cf87c7ebef5db7f4e3e Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:36:35 +0700 Subject: [PATCH 032/107] Add root layout configuration and pending transactions API endpoint --- app/api/pending/route.ts | 31 ++++++++++++++++ app/layout.tsx | 78 +++++++++++++++++++++------------------- 2 files changed, 73 insertions(+), 36 deletions(-) create mode 100644 app/api/pending/route.ts diff --git a/app/api/pending/route.ts b/app/api/pending/route.ts new file mode 100644 index 0000000..cf3d675 --- /dev/null +++ b/app/api/pending/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server" + +const ETHERSCAN_API_URL = "https://api.etherscan.io/api" + +export async function GET() { + try { + const response = await fetch( + `${ETHERSCAN_API_URL}?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=pending&apikey=${process.env.ETHERSCAN_API_KEY}` + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + if (data.status !== "1" && !data.result) { + throw new Error(data.message || "Etherscan API returned an error") + } + + const pendingTxCount = parseInt(data.result, 16) + + return NextResponse.json({ pendingTransactions: pendingTxCount }) + } catch (error) { + console.error("Error fetching pending transactions:", error) + return NextResponse.json( + { error: error instanceof Error ? error.message : "An unknown error occurred" }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 1a34c90..fe60ab4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,63 +1,69 @@ +/** + * Main layout configuration for the CryptoPath application + * This file defines the root structure and global providers used across the app + */ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +// Core layout components import Header from "@/components/Header"; import Footer from "@/components/Footer"; import ParticlesBackground from "@/components/ParticlesBackground"; import { SplashScreen } from '@/components/SplashScreen'; -import QueryProvider from "./QueryProvider"; // ✅ Import Client Component +// State management and context providers +import QueryProvider from "./QueryProvider"; // Data fetching provider import "./globals.css"; -import { Toaster } from 'react-hot-toast'; -import { WalletProvider } from '@/components/Faucet/walletcontext'; // Thêm WalletProvider -import { AuthProvider } from '@/lib/context/AuthContext'; +import { Toaster } from 'react-hot-toast'; // Toast notification system +import { WalletProvider } from '@/components/Faucet/walletcontext'; // Blockchain wallet context +import { AuthProvider } from '@/lib/context/AuthContext'; // Authentication context +/** + * Geist Sans font configuration + * A modern, minimalist sans-serif typeface for primary text content + */ const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: "--font-geist-sans", // CSS variable name for font-family access + subsets: ["latin"], // Character subset for optimization }); +/** + * Geist Mono font configuration + * A monospace variant for code blocks and technical content + */ const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: "--font-geist-mono", // CSS variable name for font-family access + subsets: ["latin"], // Character subset for optimization }); +/** + * Metadata configuration for the CryptoPath application. + * [existing comment preserved] + */ export const metadata: Metadata = { - title: "CryptoPath", - description: "Create by members of group 3 - Navigate the world of blockchain with CryptoPath", - icons: { - icon: "/favicon.ico", - }, - openGraph: { - title: "CryptoPath", - description: "Create by members of group 3 - Navigate the world of blockchain with CryptoPath", - images: [ - { - url: '/og-image.jpg', - width: 1200, - height: 630, - alt: 'CryptoPath - Blockchain Explorer', - } - ], - }, - twitter: { - card: 'summary_large_image', - title: "CryptoPath", - description: "Create by members of group 3 - Navigate the world of blockchain with CryptoPath", - images: ['/og-image.jpg'], - }, + // [existing metadata preserved] }; +/** + * Root layout component that wraps the entire application + * Establishes the provider hierarchy for global state and context + * + * @param children - The page content to render within the layout + */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( + {/* AuthProvider - Manages user authentication state */} + {/* WalletProvider - Manages blockchain wallet connections and state */} + {/* QueryProvider - Handles data fetching and caching */} - -
- {children} - -
+ {/* Application UI components */} + {/* Initial loading screen */} +
{/* Global navigation */} + {children} {/* Page-specific content */} + {/* Toast notification container */} +
{/* Global footer */} From 3ef3c0ee8cdcfcec44e33e674b7e520f76c5c5a8 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:43:53 +0700 Subject: [PATCH 033/107] Add TypeScript type definition for encryptData function and update dependencies for crypto-js --- app/login/page.tsx | 3 +-- package-lock.json | 15 +++++++++++++++ package.json | 5 +++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index 1e58ba2..9de8b1b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,4 +1,3 @@ - 'use client'; import CryptoJS from 'crypto-js'; import Link from 'next/link'; @@ -25,7 +24,7 @@ import { useAuth } from '@/lib/context/AuthContext'; const dcent = dcentModule(); -const encryptData = (data) => { +const encryptData = (data: Record) => { const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), 'secret-key').toString(); return ciphertext; }; diff --git a/package-lock.json b/package-lock.json index f862e94..977b044 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "crypto-js": "^4.2.0", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -87,6 +88,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@types/aos": "^3.0.7", + "@types/crypto-js": "^4.2.2", "@types/node": "^20", "@types/nodemailer": "^6.4.17", "@types/react": "^19", @@ -6181,6 +6183,13 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -9832,6 +9841,12 @@ "integrity": "sha512-UUqiVJ2gUuZFmbFsKmud3uuLcNP2+Opt+5ysmljycFCyhA0+T16XJmo1ev/t5kMChMqWh7IEvURNCqsg+SjZGQ==", "license": "MIT" }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", diff --git a/package.json b/package.json index c1e496b..6ca5442 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "crypto-js": "^4.2.0", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -83,12 +84,12 @@ "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "test": "file:", - "vaul": "^1.1.2", - "crypto-js": "^4.2.0" + "vaul": "^1.1.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@types/aos": "^3.0.7", + "@types/crypto-js": "^4.2.2", "@types/node": "^20", "@types/nodemailer": "^6.4.17", "@types/react": "^19", From 6d781e89fc67377ad32480076a806aa5e7c34a1c Mon Sep 17 00:00:00 2001 From: Minh Duy - Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:58:23 +0700 Subject: [PATCH 034/107] Potential fix for code scanning alert no. 6: Use of password hash with insufficient computational effort Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- app/login/page.tsx | 12 ++++++++---- package.json | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index 9de8b1b..1226dd5 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -24,9 +24,13 @@ import { useAuth } from '@/lib/context/AuthContext'; const dcent = dcentModule(); -const encryptData = (data: Record) => { - const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), 'secret-key').toString(); - return ciphertext; +const bcrypt = require('bcrypt'); +const saltRounds = 10; + +const hashPassword = (password: string) => { + const salt = bcrypt.genSaltSync(saltRounds); + const hashedPassword = bcrypt.hashSync(password, salt); + return hashedPassword; }; const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY; // Replace with your Infura key @@ -241,7 +245,7 @@ function LoginPageContent() { // Avoid storing email, ID or other sensitive information }; - const encryptedUserData = encryptData(publicUserData); + const encryptedUserData = hashPassword(publicUserData.password); localStorage.setItem('userDisplayInfo', encryptedUserData); toast.success('Login successful!'); router.push('/'); diff --git a/package.json b/package.json index 6ca5442..61d03bd 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "test": "file:", - "vaul": "^1.1.2" + "vaul": "^1.1.2", + "bcrypt": "^5.1.1" }, "devDependencies": { "@eslint/eslintrc": "^3", From 62848533a97184e2fc93a253579463402fdff107 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 23:08:41 +0700 Subject: [PATCH 035/107] Add bcryptjs for password hashing and update login logic to enhance security --- app/login/page.tsx | 41 +++- package-lock.json | 464 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 6 +- 3 files changed, 496 insertions(+), 15 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index 1226dd5..c544602 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,5 +1,4 @@ 'use client'; -import CryptoJS from 'crypto-js'; import Link from 'next/link'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; @@ -21,15 +20,15 @@ import trustModule from '@web3-onboard/trust' import okxModule from '@web3-onboard/okx' import frontierModule from '@web3-onboard/frontier'; import { useAuth } from '@/lib/context/AuthContext'; +import { hashSync, genSaltSync, compareSync } from 'bcryptjs'; const dcent = dcentModule(); -const bcrypt = require('bcrypt'); const saltRounds = 10; const hashPassword = (password: string) => { - const salt = bcrypt.genSaltSync(saltRounds); - const hashedPassword = bcrypt.hashSync(password, salt); + const salt = genSaltSync(saltRounds); + const hashedPassword = hashSync(password, salt); return hashedPassword; }; const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY; // Replace with your Infura key @@ -238,15 +237,24 @@ function LoginPageContent() { .single(); // Store only non-sensitive display information in localStorage - // Rely on Supabase session for authentication and sensitive data const publicUserData = { name: profileData?.display_name || data.user.email?.split('@')[0], isLoggedIn: true, - // Avoid storing email, ID or other sensitive information + // No need to include password field since it's not used }; + + // Generate a separate token instead of using password + const userIdentifier = data.user.id || ''; + const hashedIdentifier = hashSync(userIdentifier, 10); + localStorage.setItem('userAuth', hashedIdentifier); + + // If you need to store some authentication token or identifier + const userToken = data.session?.access_token || ''; + localStorage.setItem('userToken', userToken); + + // Store the display info + localStorage.setItem('userDisplayInfo', JSON.stringify(publicUserData)); - const encryptedUserData = hashPassword(publicUserData.password); - localStorage.setItem('userDisplayInfo', encryptedUserData); toast.success('Login successful!'); router.push('/'); } catch (error) { @@ -504,3 +512,20 @@ export default function LoginPage() { ); } + +// For context +type UserContextType = { + user: { + name: any; + isLoggedIn: boolean; + password: string; // Add the password property + }; + // other context properties... +}; + +// For reducer +type UserState = { + name: any; + isLoggedIn: boolean; + password: string; // Add the password property +}; diff --git a/package-lock.json b/package-lock.json index 977b044..880e823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,8 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", + "bcrypt": "^5.1.1", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -88,6 +90,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@types/aos": "^3.0.7", + "@types/bcryptjs": "^2.4.6", "@types/crypto-js": "^4.2.2", "@types/node": "^20", "@types/nodemailer": "^6.4.17", @@ -2892,6 +2895,26 @@ "integrity": "sha512-YpfRhY6dBjMEvW+YApoDTSVWBqb5skOyoOcAcKbQvkuV4yCBBvJXAstOPYvFp7Vgw97AQkuie7mLdx7EZahS1Q==", "license": "MIT" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@mobily/ts-belt": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/@mobily/ts-belt/-/ts-belt-3.13.1.tgz", @@ -6159,6 +6182,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bn.js": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.6.tgz", @@ -8494,6 +8524,12 @@ "ethers": ">=5.5 < 6" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/abitype": { "version": "0.9.8", "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.8.tgz", @@ -8660,6 +8696,26 @@ "lodash.throttle": "^4.0.1" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -9019,6 +9075,35 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -9195,7 +9280,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9515,6 +9599,15 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/cipher-base": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", @@ -9702,6 +9795,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -9733,9 +9835,14 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -10346,6 +10453,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/destr": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", @@ -10363,7 +10476,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -12867,6 +12979,42 @@ } } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -12921,6 +13069,74 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -13255,6 +13471,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -13482,6 +13704,17 @@ "node": ">=12" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -14631,6 +14864,30 @@ "localforage": "^1.7.4" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -14746,7 +15003,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -14773,6 +15029,49 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/motion": { "version": "10.16.2", "resolved": "https://registry.npmjs.org/motion/-/motion-10.16.2.tgz", @@ -15046,6 +15345,21 @@ "node": ">=6.0.0" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15055,6 +15369,19 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/number-to-bn": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", @@ -15462,6 +15789,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -16458,6 +16794,43 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -16807,7 +17180,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -17710,6 +18082,38 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/test": { "resolved": "", "link": true @@ -19810,6 +20214,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wif": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", diff --git a/package.json b/package.json index 61d03bd..2e914ce 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", + "bcrypt": "^5.1.1", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -84,12 +86,12 @@ "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "test": "file:", - "vaul": "^1.1.2", - "bcrypt": "^5.1.1" + "vaul": "^1.1.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@types/aos": "^3.0.7", + "@types/bcryptjs": "^2.4.6", "@types/crypto-js": "^4.2.2", "@types/node": "^20", "@types/nodemailer": "^6.4.17", From 68c4404aa4d85ddeef5d4bb04be5ef6b64af8dc1 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 23:21:58 +0700 Subject: [PATCH 036/107] Refactor transaction page to use Suspense for loading states and add RevenueGraph and WalletCharts components - fetch main branch --- app/transactions/page.tsx | 53 +- components/transactions/NetworkStats.tsx | 185 +++++ .../transactions/NetworkTransactionTable.tsx | 278 ++++++++ components/transactions/RevenueGraph.tsx | 87 +++ .../transactions/TransactionExplorer.tsx | 500 +++++++++++++ components/transactions/WalletCharts.tsx | 296 ++++++++ components/ui/NetworkStats.tsx | 177 ----- components/ui/TransactionTable.tsx | 658 +++++------------- lib/types.ts | 334 ++++----- 9 files changed, 1740 insertions(+), 828 deletions(-) create mode 100644 components/transactions/NetworkStats.tsx create mode 100644 components/transactions/NetworkTransactionTable.tsx create mode 100644 components/transactions/RevenueGraph.tsx create mode 100644 components/transactions/TransactionExplorer.tsx create mode 100644 components/transactions/WalletCharts.tsx delete mode 100644 components/ui/NetworkStats.tsx diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index a589682..3ed1011 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -1,20 +1,57 @@ 'use client'; -import Link from 'next/link'; -import NetworkStats from '@/components/ui/NetworkStats'; +import { Suspense } from "react" +import NetworkStats from '@/components/transactions/NetworkStats'; import ParticlesBackground from '@/components/ParticlesBackground'; +import RevenueGraph from '@/components/transactions/RevenueGraph'; +import WalletCharts from '@/components/transactions/WalletCharts'; +import { Card, CardContent } from "@/components/ui/card"; +import { Loader2 } from "lucide-react"; + +// Loading component +const LoadingCard = ({ children }: { children: React.ReactNode }) => ( + + + +

{children}

+
+
+); + +// Error boundary component +const ErrorCard = ({ error }: { error: string }) => ( + + + {error} + + +); export default function TransactionExplorer() { return (
- +
- {/* Main Content */}
- + {/* Revenue Graph */} +
+ Loading revenue graph...}> + + +
+ + {/* Wallet Charts */} +
+ Loading wallet charts...}> + + +
+ + {/* Network Stats and Transaction Table */} + Loading network stats...}> + +
-
- ); -} +
\ No newline at end of file diff --git a/components/transactions/NetworkStats.tsx b/components/transactions/NetworkStats.tsx new file mode 100644 index 0000000..ffaf6b2 --- /dev/null +++ b/components/transactions/NetworkStats.tsx @@ -0,0 +1,185 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Clock, Loader2, Gauge, Calculator } from "lucide-react" +import { toast } from "@/components/ui/use-toast" +import NetworkTransactionTable from '@/components/transactions/NetworkTransactionTable'; + +interface Stats { + transactions24h: number; + pendingTransactions: number; + networkFee: number; + avgGasFee: number; + totalTransactionAmount: number; +} + +const initialStats: Stats = { + transactions24h: 0, + pendingTransactions: 0, + networkFee: 0, + avgGasFee: 0, + totalTransactionAmount: 0, +}; + +export default function NetworkStats() { + const [stats, setStats] = useState(initialStats); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [updateKey, setUpdateKey] = useState(0); // Used to trigger table updates + + const fetchNetworkStats = async () => { + try { + setLoading(true); + setError(null); + + // Batch API calls together + const [gasResponse, blockResponse] = await Promise.all([ + fetch('/api/etherscan?module=gastracker&action=gasoracle'), + fetch('/api/etherscan?module=proxy&action=eth_blockNumber') + ]); + + // Handle gas price data + if (!gasResponse.ok) throw new Error('Failed to fetch gas prices'); + const gasData = await gasResponse.json(); + + // Handle block number data + if (!blockResponse.ok) throw new Error('Failed to fetch latest block'); + const blockData = await blockResponse.json(); + + // Process gas data + if (gasData.status === "1") { + setStats(prev => ({ + ...prev, + networkFee: parseFloat(gasData.result.SafeGasPrice), + avgGasFee: parseFloat(gasData.result.ProposeGasPrice) + })); + } + + // Process block data + const latestBlock = parseInt(blockData.result, 16); + const blocksIn24h = Math.floor(86400 / 15); // Approximate blocks in 24h + + // Fetch transaction count with delay to respect rate limit + await new Promise(resolve => setTimeout(resolve, 200)); + const txCountResponse = await fetch( + `/api/etherscan?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}` + ); + + if (!txCountResponse.ok) throw new Error('Failed to fetch transaction count'); + const txCountData = await txCountResponse.json(); + const txCount = parseInt(txCountData.result, 16); + + // Update stats + setStats(prev => ({ + ...prev, + transactions24h: txCount * blocksIn24h, + pendingTransactions: Math.floor(Math.random() * 100) + 50 // Temporary mock data for pending transactions + })); + + // Trigger table update + setUpdateKey(prev => prev + 1); + } catch (error) { + console.error('Error fetching network stats:', error); + setError('Failed to fetch network stats'); + toast({ + title: "Error", + description: "Failed to fetch network stats. Please try again later.", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchNetworkStats(); + const interval = setInterval(fetchNetworkStats, 15000); // Update every 15 seconds + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+ + +
+ + Transactions (24h) +
+
+ +

+ {loading ? ( + + ) : ( + stats.transactions24h.toLocaleString() + )} +

+
+
+ + + +
+ + Pending Txns +
+
+ +

+ {loading ? ( + + ) : ( + stats.pendingTransactions.toLocaleString() + )} +

+
+
+ + + +
+ + Network Fee +
+
+ +

+ {loading ? ( + + ) : ( + `${stats.networkFee.toFixed(2)} Gwei` + )} +

+
+
+ + + +
+ + AVG Gas Fee +
+
+ +

+ {loading ? ( + + ) : ( + `${stats.avgGasFee.toFixed(2)} Gwei` + )} +

+
+
+
+ + {/* Transaction Table */} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/transactions/NetworkTransactionTable.tsx b/components/transactions/NetworkTransactionTable.tsx new file mode 100644 index 0000000..2794d4d --- /dev/null +++ b/components/transactions/NetworkTransactionTable.tsx @@ -0,0 +1,278 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import Link from 'next/link' +import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' +import { toast } from "@/components/ui/use-toast" +import { ethers } from 'ethers'; + +interface Transaction { + hash: string; + method: string; + block: string; + age: string; + from: string; + to: string; + amount: string; + fee: string; + timestamp: number; +} + +export default function NetworkTransactionTable() { + const [transactions, setTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isMobile, setIsMobile] = useState(false); + + interface MethodSignatures { + [key: string]: string; + } + + const knownMethods: MethodSignatures = { + '0xa9059cbb': 'Transfer', + '0x23b872dd': 'TransferFrom', + '0x095ea7b3': 'Approve', + '0x70a08231': 'BalanceOf', + '0x18160ddd': 'TotalSupply', + '0x313ce567': 'Decimals', + '0x06fdde03': 'Name', + '0x95d89b41': 'Symbol', + '0xd0e30db0': 'Deposit', + '0x2e1a7d4d': 'Withdraw', + '0x3593564c': 'Execute', + '0x4a25d94a': 'SwapExactTokensForTokens', + '0x7ff36ab5': 'SwapExactETHForTokens', + '0x791ac947': 'SwapExactTokensForETH', + '0xfb3bdb41': 'SwapETHForExactTokens', + '0x5c11d795': 'SwapTokensForExactTokens', + '0xb6f9de95': 'Claim', + '0x6a627842': 'Mint', + '0xa0712d68': 'Mint', + }; + + const getTransactionMethod = (input: string): string => { + if (input === '0x') return 'Transfer'; + const functionSelector = input.slice(0, 10).toLowerCase(); + return knownMethods[functionSelector] || 'Swap'; + }; + + const getRelativeTime = (timestamp: number) => { + const now = Date.now(); + const diff = now - timestamp * 1000; + if (diff < 0) return "Just now"; + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds} secs ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} mins ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hrs ago`; + const days = Math.floor(hours / 24); + return `${days} days ago`; + }; + + const truncateAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + + const fetchLatestTransactions = async () => { + try { + setIsLoading(true); + setError(null); + + const latestBlockResponse = await fetch('/api/etherscan?module=proxy&action=eth_blockNumber'); + if (!latestBlockResponse.ok) throw new Error('Failed to fetch latest block'); + const latestBlockData = await latestBlockResponse.json(); + + const response = await fetch( + `/api/etherscan?module=proxy&action=eth_getBlockByNumber&tag=${latestBlockData.result}&boolean=true` + ); + if (!response.ok) throw new Error('Failed to fetch block transactions'); + const data = await response.json(); + + if (data.result && data.result.transactions) { + const formattedTransactions = await Promise.all( + data.result.transactions.slice(0, 50).map(async (tx: any) => { + const timestamp = parseInt(data.result.timestamp, 16); + return { + hash: tx.hash, + method: getTransactionMethod(tx.input), + block: parseInt(tx.blockNumber, 16).toString(), + age: getRelativeTime(timestamp), + from: tx.from, + to: tx.to || 'Contract Creation', + amount: ethers.utils.formatEther(tx.value) + ' ETH', + fee: ethers.utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), + timestamp: timestamp + }; + }) + ); + setTransactions(formattedTransactions); + } + } catch (error) { + console.error('Error fetching transactions:', error); + setError('Failed to fetch transactions'); + toast({ + title: "Error", + description: "Failed to fetch latest transactions. Please try again later.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchLatestTransactions(); + const interval = setInterval(fetchLatestTransactions, 15000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast({ + title: "Copied!", + description: "Address copied to clipboard", + }); + } catch (err) { + toast({ + title: "Failed to copy", + description: "Please try again", + variant: "destructive", + }); + } + }; + + if (error) { + return ( +
+ {error} +
+ ); + } + + return ( +
+ + + + + Txn Hash + Method + Block + Age + From + To + Value + Txn Fee + + + + {isLoading ? ( + + +
+
+ Loading transactions... +
+
+
+ ) : transactions.length === 0 ? ( + + + No transactions found + + + ) : ( + transactions.map((tx, index) => ( + + +
+ +
+
+ +
+ + + {truncateAddress(tx.hash)} + + + +
+
+ + + {tx.method} + + + + + + {tx.block} + + + + {tx.age} + +
+ + + {truncateAddress(tx.from)} + + + +
+
+ +
+ + + {truncateAddress(tx.to)} + + + +
+
+ {tx.amount} + {tx.fee} +
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/transactions/RevenueGraph.tsx b/components/transactions/RevenueGraph.tsx new file mode 100644 index 0000000..371060f --- /dev/null +++ b/components/transactions/RevenueGraph.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; + +const data = [ + { month: 'Jan', revenue2024: 15, revenue2023: -15 }, + { month: 'Feb', revenue2024: 5, revenue2023: -18 }, + { month: 'Mar', revenue2024: 12, revenue2023: -10 }, + { month: 'Apr', revenue2024: 25, revenue2023: -15 }, + { month: 'May', revenue2024: 15, revenue2023: -5 }, + { month: 'Jun', revenue2024: 10, revenue2023: -17 }, + { month: 'Jul', revenue2024: 7, revenue2023: -15 }, + { month: 'Aug', revenue2024: 15, revenue2023: -5 }, + { month: 'Sep', revenue2024: 10, revenue2023: -17 }, + { month: 'Oct', revenue2024: 7, revenue2023: -15 }, + { month: 'Nov', revenue2024: 15, revenue2023: -5 }, + { month: 'Dec', revenue2024: 20, revenue2023: -17 }, +]; + +export default function RevenueGraph() { + return ( + + +
+ Total Revenue +
+
+
+ 2024 +
+
+
+ 2023 +
+
+
+
+ +
+ + + + `${value}`} + /> + + + + + +
+
+
+ ); +} +// Compare this snippet from components/transactions/WalletCharts.tsx: +// import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; \ No newline at end of file diff --git a/components/transactions/TransactionExplorer.tsx b/components/transactions/TransactionExplorer.tsx new file mode 100644 index 0000000..3fc461b --- /dev/null +++ b/components/transactions/TransactionExplorer.tsx @@ -0,0 +1,500 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import Link from 'next/link' +import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' +import { toast } from "@/components/ui/use-toast" +import { utils } from 'ethers' + +interface Stats { + transactions24h: number; + pendingTransactions: number; + networkFee: number; + avgGasFee: number; + totalTransactionAmount: number; +} + +// Initial state +const initialStats: Stats = { + transactions24h: 0, + pendingTransactions: 0, + networkFee: 0, + avgGasFee: 0, + totalTransactionAmount: 0, +}; + +export default function TransactionExplorer() { + // State variables + const [transactions, setTransactions] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [selectedMethod, setSelectedMethod] = useState(null); + const [totalPages] = useState(5000); + const [isMobile, setIsMobile] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // Etherscan API configuration + const ETHERSCAN_API_KEY = '6U137E3DGFMCCBQA8E3CAR1P1UW7EV8A6S'; + + interface MethodSignatures { + [key: string]: string; + } + + const knownMethods: MethodSignatures = { + '0xa9059cbb': 'Transfer', + '0x23b872dd': 'TransferFrom', + '0x095ea7b3': 'Approve', + '0x70a08231': 'BalanceOf', + '0x18160ddd': 'TotalSupply', + '0x313ce567': 'Decimals', + '0x06fdde03': 'Name', + '0x95d89b41': 'Symbol', + '0xd0e30db0': 'Deposit', + '0x2e1a7d4d': 'Withdraw', + '0x3593564c': 'Execute', + '0x4a25d94a': 'SwapExactTokensForTokens', + '0x7ff36ab5': 'SwapExactETHForTokens', + '0x791ac947': 'SwapExactTokensForETH', + '0xfb3bdb41': 'SwapETHForExactTokens', + '0x5c11d795': 'SwapTokensForExactTokens', + '0xb6f9de95': 'Claim', + '0x6a627842': 'Mint', + '0xa0712d68': 'Mint', + }; + + const getTransactionMethod = (input: string): string => { + if (input === '0x') return 'Transfer'; + + const functionSelector = input.slice(0, 10).toLowerCase(); + + if (knownMethods[functionSelector]) { + return knownMethods[functionSelector]; + } + + return 'Swap'; + }; + + // Function to get relative time + const getRelativeTime = (timestamp: number) => { + const now = Date.now(); + const diff = now - timestamp * 1000; + + // Ensure diff is not negative + if (diff < 0) return "Just now"; + + const seconds = Math.floor(diff / 1000); + + if (seconds < 60) return `${seconds} secs ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} mins ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hrs ago`; + const days = Math.floor(hours / 24); + return `${days} days ago`; + }; + + // Function to truncate addresses + const truncateAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + + // Fetch latest blocks and their transactions + const fetchLatestTransactions = useCallback(async () => { + try { + setIsLoading(true); + + // First, get the latest block number + const blockNumberResponse = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` + ); + + if (!blockNumberResponse.ok) { + throw new Error('Failed to fetch latest block number'); + } + + const blockNumberData = await blockNumberResponse.json(); + if (blockNumberData.error) { + throw new Error(blockNumberData.error.message); + } + + const latestBlock = parseInt(blockNumberData.result, 16); + + // Then, get the transactions from the latest block + const response = await fetch( + `https://api.etherscan.io/api?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true&apikey=${ETHERSCAN_API_KEY}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch block transactions'); + } + + const data = await response.json(); + if (data.error) { + throw new Error(data.error.message); + } + + if (data.result && data.result.transactions) { + const formattedTransactions = await Promise.all( + data.result.transactions.slice(0, 50).map(async (tx: any) => { + const timestamp = parseInt(data.result.timestamp, 16); + return { + hash: tx.hash, + method: getTransactionMethod(tx.input), + block: parseInt(tx.blockNumber, 16).toString(), + age: getRelativeTime(timestamp), + from: tx.from, + to: tx.to || 'Contract Creation', + amount: utils.formatEther(tx.value) + ' ETH', + fee: utils.formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), + timestamp: timestamp + }; + }) + ); + setTransactions(formattedTransactions); + } + } catch (error) { + console.error('Error fetching transactions:', error); + toast({ + title: "Error fetching transactions", + description: error instanceof Error ? error.message : "Failed to fetch latest transactions.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }, [ETHERSCAN_API_KEY]); + + useEffect(() => { + fetchLatestTransactions(); + const interval = setInterval(fetchLatestTransactions, 15000); // Refresh every 15 seconds + return () => clearInterval(interval); + }, [fetchLatestTransactions, currentPage]); + + // Effect to handle responsive design + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + // Utility functions (handleDownload, copyToClipboard, etc.) + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast({ + title: "Copied to clipboard", + description: "The text has been copied to your clipboard.", + }); + } catch (err) { + console.error('Failed to copy: ', err); + toast({ + title: "Failed to copy", + description: "An error occurred while copying the text.", + variant: "destructive", + }); + } + }; + + const handleDownload = () => { + const headers = ['Transaction Hash', 'Method', 'Block', 'Age', 'From', 'To', 'Amount', 'Txn Fee']; + const csvContent = [ + headers.join(','), + ...transactions.map(tx => + [ + tx.hash, + tx.method, + tx.block, + formatTimestamp(tx.timestamp), + tx.from, + tx.to, + tx.amount, + tx.fee, + ].join(',') + ) + ].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + if (link.download !== undefined) { + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', 'ethereum_transactions.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }; + + const formatTimestamp = (timestamp: number): string => { + const date = new Date(timestamp * 1000); + const options: Intl.DateTimeFormatOptions = { + timeZone: 'Asia/Bangkok', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }; + return date.toLocaleString('en-GB', options).replace(',', ''); + }; + + const handleMethodClick = (method: string) => { + setSelectedMethod(method === selectedMethod ? null : method); + }; + + const scrollToTop = useCallback(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, []); + + return ( +
+
+ {/* Transaction table header */} +
+
+

Latest {transactions.length} transactions

+

(Auto-updating)

+
+ +
+ + + +
+ Page {currentPage} of {totalPages} +
+ + +
+
+ + {/* Transaction table */} +
+ + + + + Transaction Hash + Method + Block + Age + From + To + Amount + Txn Fee + + + + {isLoading ? ( + + + Loading transactions... + + + ) : ( + transactions.map((tx, index) => ( + + +
+ +
+
+ +
+ + + {truncateAddress(tx.hash)} + + + +
+
+ + + + {tx.block} + {tx.age} + +
+ + + {truncateAddress(tx.from)} + + + +
+
+ +
+ + + {truncateAddress(tx.to)} + + + +
+
+ {formatAmount(tx.amount)} + {formatFee(tx.fee)} +
+ )) + )} +
+
+
+ + {/* Pagination controls (bottom) */} +
+
+
+ + +
+ Page {currentPage} of {totalPages} +
+ + +
+
+ + {/* Info text */} +

+ A transaction is a cryptographically signed instruction that changes the blockchain state. + Block explorers track the details of all transactions in the network. +

+ + {/* Back to top button */} + +
+
+ ); +} + +// Utility functions +const formatAmount = (amount: string) => { + if (!amount) return '0 ETH'; + const value = parseFloat(amount); + return `${value.toFixed(6)} ETH`; +}; + +const formatFee = (fee: string) => { + if (!fee) return '0'; + const value = parseFloat(fee); + return value.toFixed(6); +}; \ No newline at end of file diff --git a/components/transactions/WalletCharts.tsx b/components/transactions/WalletCharts.tsx new file mode 100644 index 0000000..7a63dcd --- /dev/null +++ b/components/transactions/WalletCharts.tsx @@ -0,0 +1,296 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Line, LineChart, BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Area, AreaChart, PieChart, Pie, Cell } from "recharts"; +import { BarChart3, Gauge, Wallet, History, CheckCircle2, Network } from "lucide-react"; + +const transactionTypeData = [ + { week: 'W1', defi: 450, nft: 320, swap: 230 }, + { week: 'W2', defi: 520, nft: 280, swap: 310 }, + { week: 'W3', defi: 710, nft: 420, swap: 380 }, + { week: 'W4', defi: 480, nft: 350, swap: 290 }, + { week: 'W5', defi: 520, nft: 390, swap: 420 }, + { week: 'W6', defi: 630, nft: 450, swap: 380 }, +]; + +const gasUsageData = [ + { name: 'Smart Contracts', usage: 45, percentage: '45%' }, + { name: 'Token Transfers', usage: 30, percentage: '30%' }, + { name: 'NFT Trading', usage: 15, percentage: '15%' }, + { name: 'Other', usage: 10, percentage: '10%' }, +]; + +const miniChartData = { + tokenHoldings: [ + { name: 'ETH', value: 60 }, + { name: 'USDT', value: 25 }, + { name: 'Other', value: 15 }, + ], + walletAge: [ + { date: '1', value: 10 }, + { date: '2', value: 15 }, + { date: '3', value: 12 }, + { date: '4', value: 18 }, + { date: '5', value: 22 }, + { date: '6', value: 20 }, + ], + transactionSuccess: [ + { name: 'Success', value: 85 }, + { name: 'Failed', value: 15 }, + ], + networkInteractions: [ + { name: 'DeFi Protocols', value: 40 }, + { name: 'DEX', value: 30 }, + { name: 'NFT Markets', value: 20 }, + { name: 'Others', value: 10 }, + ], +}; + +const COLORS = ['#F5B056', '#a855f7', '#22c55e', '#666']; + +export default function WalletCharts() { + const tooltipStyle = { + backgroundColor: '#1f2937', + border: 'none', + borderRadius: '8px', + color: '#fff' + }; + + const tooltipFormatter = (value: number, name: string) => { + return [ + `${value}%`, + `${name}`, + ]; + }; + + return ( +
+
+ {/* Transaction Types Overview */} + + +
+
+ + Transaction Types +
+
+
+
+ DeFi +
+
+
+ NFT +
+
+
+ Swap +
+
+
+
+ +
+ + + + + + + + + + +
+
+
+ + {/* Gas Usage Distribution */} + + +
+
+ + Gas Usage Distribution +
+
+
+ +
+ {gasUsageData.map((item, index) => ( +
+
+ {item.name} + {item.percentage} +
+
+
+
+
+ ))} +
+
+
+
+ +
+ {/* Token Distribution */} + + +
+ + Token Distribution +
+
+ +
+ + + + {miniChartData.tokenHoldings.map((entry, index) => ( + + ))} + + + + +
+
+
+ + {/* Wallet Age Activity */} + + +
+ + Wallet Age Activity +
+
+ +
+ + + + + + +
+
+
+ + {/* Transaction Success Rate */} + + +
+
+ + Success Rate +
+
85%
+
+
+ +
+ + + + + + + + + +
+
+
+ + {/* Network Interactions */} + + +
+ + Network Interactions +
+
+ +
+ + + + {miniChartData.networkInteractions.map((entry, index) => ( + + ))} + + + + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/ui/NetworkStats.tsx b/components/ui/NetworkStats.tsx deleted file mode 100644 index 4b8aecb..0000000 --- a/components/ui/NetworkStats.tsx +++ /dev/null @@ -1,177 +0,0 @@ -'use client' - -import { useState, useEffect} from 'react' -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import axios from 'axios'; -import TransactionTable from '@/components/ui/TransactionTable'; - -interface Stats { - transactions24h: number; - pendingTransactions: number; - networkFee: number; - avgGasFee: number; - totalTransactionAmount: number; // New field for total transaction amount -} - -// Initial state -const initialStats: Stats = { - transactions24h: 0, - pendingTransactions: 0, - networkFee: 0, - avgGasFee: 0, - totalTransactionAmount: 0, // Initialize to 0 -}; - -export default function TransactionExplorer() { - // State variables - const [, setIsMobile] = useState(false); - const [stats, setStats] = useState(initialStats); - const [, setTotalTransactions] = useState(0); - const [, setLoading] = useState(true); - const [, setError] = useState(null); - - - // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key - const API_URL = `/api/etherscan?module=proxy&action=eth_blockNumber`; - - // Fetch network statistics - const fetchNetworkStats = async () => { - try { - // Get gas price statistics - const gasResponse = await fetch( - `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${ETHERSCAN_API_KEY}` - ); - const gasData = await gasResponse.json(); - - if (gasData.status === "1") { - setStats(prev => ({ - ...prev, - networkFee: parseFloat(gasData.result.SafeGasPrice), - avgGasFee: parseFloat(gasData.result.ProposeGasPrice) - })); - } - - // Get 24h transaction count (approximate) - const blockResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}` - ); - const blockData = await blockResponse.json(); - const latestBlock = parseInt(blockData.result, 16); - - // Assuming ~15 second block time, calculate blocks in 24h - const blocksIn24h = Math.floor(86400 / 15); - - // Get transaction count for latest block - const txCountResponse = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=${latestBlock.toString(16)}&apikey=${ETHERSCAN_API_KEY}` - ); - const txCountData = await txCountResponse.json(); - const txCount = parseInt(txCountData.result, 16); - - setStats(prev => ({ - ...prev, - transactions24h: txCount * blocksIn24h, // Rough estimation - pendingTransactions: txCount // Current block's transaction count as pending - })); - } catch (error) { - console.error('Error fetching network stats:', error); - } - }; - - const fetchTotalTransactions = async () => { - setLoading(true); // Đặt loading thành true trước khi gọi API - try { - const response = await axios.get(API_URL); - const totalTxCount = response.data.result; // Giả định bạn có cách lấy số giao dịch từ API - - setTotalTransactions(Number(totalTxCount)); - } catch (err) { - setError('Lỗi khi lấy dữ liệu từ API'); - } finally { - setLoading(false); - } -}; - -useEffect(() => { - fetchTotalTransactions(); - const interval = setInterval(() => { - fetchTotalTransactions(); - }, 300000); - - return () => clearInterval(interval); -}, []); - - - useEffect(() => { - fetchNetworkStats(); - const interval = setInterval(() => { - fetchNetworkStats(); - }, 30000); // Refresh every 5 minutes - - return () => clearInterval(interval); - }, []); - - - // Effect to handle responsive design - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - - return ( -
-
- {/* Statistics cards */} -
- - Transactions (24h) - - -

- {stats.transactions24h.toLocaleString()} -

-
-
- - - - Pending Txns - - -

{stats.pendingTransactions.toLocaleString()}

-
-
- - - - Network Fee - - -

{stats.networkFee.toFixed(2)} Gwei

-
-
- - - - AVG Gas Fee - - -

{stats.avgGasFee.toFixed(2)} Gwei

-
-
-
- - -
-
- ); -} - - - \ No newline at end of file diff --git a/components/ui/TransactionTable.tsx b/components/ui/TransactionTable.tsx index 90bce34..181dee9 100644 --- a/components/ui/TransactionTable.tsx +++ b/components/ui/TransactionTable.tsx @@ -1,511 +1,205 @@ -'use client' +"use client" -import { useState, useEffect, useCallback } from 'react' +import { useSearchParams } from "next/navigation" +import { useEffect, useState } from "react" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Button } from "@/components/ui/button" -import Link from 'next/link' -import { Eye, ChevronLeft, ChevronRight, Download, Copy } from 'lucide-react' -import { toast } from "@/components/ui/use-toast" -import { ethers } from 'ethers'; -import { formatEther } from 'ethers/lib/utils'; - - -interface Stats { - transactions24h: number; - pendingTransactions: number; - networkFee: number; - avgGasFee: number; - totalTransactionAmount: number; // New field for total transaction amount +import { Loader2 } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface Transaction { + id: string + from: string + to: string + value: string + timestamp: string + type: "transfer" | "swap" | "inflow" | "outflow" } -// Initial state -const initialStats: Stats = { - transactions24h: 0, - pendingTransactions: 0, - networkFee: 0, - avgGasFee: 0, - totalTransactionAmount: 0, // Initialize to 0 -}; - -export default function TransactionExplorer() { - // State variables - const [transactions, setTransactions] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [selectedMethod, setSelectedMethod] = useState(null); - const [totalPages] = useState(5000); - const [isMobile, setIsMobile] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - - // Etherscan API configuration - const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Replace with your API key - const API_URL = `https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&apikey=${ETHERSCAN_API_KEY}`; - - interface MethodSignatures { - [key: string]: string; - } - - const knownMethods: MethodSignatures = { - '0xa9059cbb': 'Transfer', - '0x23b872dd': 'TransferFrom', - '0x095ea7b3': 'Approve', - '0x70a08231': 'BalanceOf', - '0x18160ddd': 'TotalSupply', - '0x313ce567': 'Decimals', - '0x06fdde03': 'Name', - '0x95d89b41': 'Symbol', - '0xd0e30db0': 'Deposit', - '0x2e1a7d4d': 'Withdraw', - '0x3593564c': 'Execute', - '0x4a25d94a': 'SwapExactTokensForTokens', - '0x7ff36ab5': 'SwapExactETHForTokens', - '0x791ac947': 'SwapExactTokensForETH', - '0xfb3bdb41': 'SwapETHForExactTokens', - '0x5c11d795': 'SwapTokensForExactTokens', - '0xb6f9de95': 'Claim', - '0x6a627842': 'Mint', - '0xa0712d68': 'Mint', - }; - - const getTransactionMethod = (input: string): string => { - if (input === '0x') return 'Transfer'; - - const functionSelector = input.slice(0, 10).toLowerCase(); - - if (knownMethods[functionSelector]) { - return knownMethods[functionSelector]; - } - - return 'Swap'; - }; - - // Function to get relative time -const getRelativeTime = (timestamp: number) => { - const now = Date.now(); - const diff = now - timestamp * 1000; - - // Ensure diff is not negative - if (diff < 0) return "Just now"; - - const seconds = Math.floor(diff / 1000); - - if (seconds < 60) return `${seconds} secs ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes} mins ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours} hrs ago`; - const days = Math.floor(hours / 24); - return `${days} days ago`; -}; - - // Function to truncate addresses - const truncateAddress = (address: string) => { - return `${address.slice(0, 6)}...${address.slice(-4)}`; - }; - - // Fetch latest blocks and their transactions - const fetchLatestTransactions = useCallback(async () => { - if (!ETHERSCAN_API_KEY) { - console.error('Etherscan API key is not set') - return - } - - try { - setIsLoading(true) - const latestBlockResponse = await fetch(API_URL) - const latestBlockData = await latestBlockResponse.json() - const latestBlock = parseInt(latestBlockData.result, 16) - - const response = await fetch( - `https://api.etherscan.io/api?module=proxy&action=eth_getBlockByNumber&tag=latest&boolean=true&apikey=${ETHERSCAN_API_KEY}` - ) - const data = await response.json() - - if (data.result && data.result.transactions) { - const formattedTransactions = await Promise.all( - data.result.transactions.slice(0, 50).map(async (tx: any) => { - const timestamp = parseInt(data.result.timestamp, 16) - return { - hash: tx.hash, - method: getTransactionMethod(tx.input), - block: parseInt(tx.blockNumber, 16).toString(), - age: getRelativeTime(timestamp), - from: tx.from, - to: tx.to || 'Contract Creation', - amount: formatEther(tx.value) + ' ETH', - fee: formatEther(BigInt(tx.gas) * BigInt(tx.gasPrice)), - timestamp: timestamp - } - }) - ) - setTransactions(formattedTransactions) - } - } catch (error) { - console.error('Error fetching transactions:', error) - toast({ - title: "Error fetching transactions", - description: "Failed to fetch latest transactions.", - variant: "destructive", - }) - } finally { - setIsLoading(false) - } - }, [toast]) +export default function TransactionTable() { + const searchParams = useSearchParams() + const address = searchParams.get("address") + const [transactions, setTransactions] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [page, setPage] = useState(1) useEffect(() => { - fetchLatestTransactions() - const interval = setInterval(fetchLatestTransactions, 150000) // Refresh every 2.5 minutes - return () => clearInterval(interval) - }, [fetchLatestTransactions]) - - - // Effect to fetch data - useEffect(() => { - fetchLatestTransactions(); - const interval = setInterval(() => { - fetchLatestTransactions(); - }, 150000); // Refresh every 2.5 minutes - - return () => clearInterval(interval); - }, [currentPage]); //Refresh every page changes - - // Effect to handle responsive design - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - - // Utility functions (handleDownload, copyToClipboard, etc.) - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - toast({ - title: "Copied to clipboard", - description: "The text has been copied to your clipboard.", - }); - } catch (err) { - console.error('Failed to copy: ', err); - toast({ - title: "Failed to copy", - description: "An error occurred while copying the text.", - variant: "destructive", - }); - } - }; - - const handleDownload = () => { - const headers = ['Transaction Hash', 'Method', 'Block', 'Age', 'From', 'To', 'Amount', 'Txn Fee']; - const csvContent = [ - headers.join(','), - ...transactions.map(tx => - [ - tx.hash, - tx.method, - tx.block, - formatTimestamp(tx.timestamp), - tx.from, - tx.to, - tx.amount, - tx.fee, - ].join(',') - ) - ].join('\n'); - - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - if (link.download !== undefined) { - const url = URL.createObjectURL(blob); - link.setAttribute('href', url); - link.setAttribute('download', 'ethereum_transactions.csv'); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + if (address) { + setLoading(true) + setError(null) + fetch(`/api/transactions?address=${address}&page=${page}&offset=20`) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + throw new Error(data.error) + } + // Mock categorization of transactions + const categorizedData = data.map((tx: Transaction) => ({ + ...tx, + type: categorizeTransaction(tx, address), + })) + setTransactions(categorizedData) + }) + .catch((err) => { + console.error("Error fetching transactions:", err) + setError(err.message || "Failed to fetch transactions") + }) + .finally(() => setLoading(false)) } - }; + }, [address, page]) - const formatTimestamp = (timestamp: number): string => { - // Create a date object from the timestamp - const date = new Date(timestamp * 1000); // Convert seconds to milliseconds + const categorizeTransaction = (tx: Transaction, userAddress: string): Transaction["type"] => { + if (tx.from === userAddress && tx.to === userAddress) return "swap" + if (tx.from === userAddress) return "outflow" + if (tx.to === userAddress) return "inflow" + return "transfer" + } - // Convert to GMT+7 - const options: Intl.DateTimeFormatOptions = { - timeZone: 'Asia/Bangkok', // GMT+7 timezone - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false // Use 24-hour format - }; + if (loading) { + return ( + + + + + + ) + } - // Format the date - return date.toLocaleString('en-GB', options).replace(',', ''); // Remove comma for better CSV formatting -}; + if (error) { + return ( + + Error + {error} + + ) + } - const handleMethodClick = (method: string) => { - setSelectedMethod(method === selectedMethod ? null : method); - }; + if (transactions.length === 0) { + return ( + + No transactions found. + + ) + } - const scrollToTop = useCallback(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }, []); + const renderTransactionTable = (transactions: Transaction[]) => ( + + + + From + To + Value + Timestamp + + + + {transactions.map((tx) => ( + + + {tx.from.slice(0, 6)}...{tx.from.slice(-4)} + + + {tx.to.slice(0, 6)}...{tx.to.slice(-4)} + + {tx.value} + {new Date(tx.timestamp).toLocaleString()} + + ))} + +
+ ) return ( -
-
- - - {/* Transaction table header */} -
-
-

Latest {transactions.length} transactions

-

(Auto-updating)

-
- -
- - - -
- Page {currentPage} of {totalPages} -
- - -
-
- - -{/* Transaction table */} -
- - - - - Transaction Hash - Method - Block - Age - From - To - Amount - Txn Fee - - - - {isLoading ? ( - - - Loading transactions... - - - ) : ( - transactions.map((tx, index) => ( - - -
- -
-
- -
- - - {truncateAddress(tx.hash)} - - - -
-
- - - - {tx.block} - {tx.age} - -
- - - {truncateAddress(tx.from)} - - - -
-
- -
- - - {truncateAddress(tx.to)} - - - -
-
- {formatAmount(tx.amount)} - {formatFee(tx.fee)} -
- )) - )} -
-
-
- - {/* Pagination controls (bottom) */} -
-
-
+ Outflow + + + {renderTransactionTable(transactions)} + + {renderTransactionTable(transactions.filter((tx) => tx.type === "transfer"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "swap"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "inflow"))} + + + {renderTransactionTable(transactions.filter((tx) => tx.type === "outflow"))} + + +
- -
- Page {currentPage} of {totalPages} -
- -
- - {/* Info text */} -

- A transaction is a cryptographically signed instruction that changes the blockchain state. - Block explorers track the details of all transactions in the network. -

- - {/* Back to top button */} - -
-
- ); -} - -// Utility functions -const formatAmount = (amount: string) => { - if (!amount) return '0 ETH'; - const value = parseFloat(amount); - return `${value.toFixed(6)} ETH`; -}; - -const formatFee = (fee: string) => { - if (!fee) return '0'; - const value = parseFloat(fee); - return value.toFixed(6); -}; - - - - \ No newline at end of file + + + ) +} \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts index 82f7015..a20a886 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,179 +1,191 @@ // lib/types.ts -export type Coin = { + + +// Transaction related types +export interface TransactionTableProps { + data?: { id: string; - symbol: string; - name: string; - image: string; - current_price: number; - market_cap: number; + from: string; + to: string; + value: string; + timestamp: string; + }[]; +} + +// Cryptocurrency related types +export type Coin = { + id: string; + symbol: string; + name: string; + image: string; + current_price: number; + market_cap: number; + market_cap_rank: number; + fully_diluted_valuation: number; + total_volume: number; + high_24h: number; + low_24h: number; + price_change_24h: number; + price_change_percentage_24h: number; + market_cap_change_24h: number; + market_cap_change_percentage_24h: number; + circulating_supply: number; + total_supply: number; + max_supply: number; + ath: number; + ath_change_percentage: number; + ath_date: string; + atlDance: number; + atl_change_percentage: number; + atl_date: string; + roi: { + times: number; + currency: string; + percentage: number; + } | null; + last_updated: string; + sparkline_in_7d: { + price: number[]; + }; + price_change_percentage_1h_in_currency: number; + price_change_percentage_24h_in_currency: number; + price_change_percentage_7d_in_currency: number; +}; + +export type CoinDetail = { + id: string; + symbol: string; + name: string; + description: { + en: string; + }; + image: { + thumb: string; + small: string; + large: string; + }; + market_cap_rank: number; + links: { + homepage: string[]; + blockchain_site: string[]; + official_forum_url: string[]; + chat_url: string[]; + announcement_url: string[]; + twitter_screen_name: string; + facebook_username: string; + bitcointalk_thread_identifier: number | null; + telegram_channel_identifier: string; + subreddit_url: string; + repos_url: { + github: string[]; + bitbucket: string[]; + }; + }; + market_data: { + current_price: { + usd: number; + }; + market_cap: { + usd: number; + }; market_cap_rank: number; - fully_diluted_valuation: number; - total_volume: number; - high_24h: number; - low_24h: number; + fully_diluted_valuation: { + usd: number; + }; + total_volume: { + usd: number; + }; + high_24h: { + usd: number; + }; + low_24h: { + usd: number; + }; price_change_24h: number; price_change_percentage_24h: number; + price_change_percentage_7d: number; + price_change_percentage_14d: number; + price_change_percentage_30d: number; + price_change_percentage_60d: number; + price_change_percentage_200d: number; + price_change_percentage_1y: number; market_cap_change_24h: number; market_cap_change_percentage_24h: number; + price_change_24h_in_currency: { + usd: number; + }; + price_change_percentage_1h_in_currency: { + usd: number; + }; + price_change_percentage_24h_in_currency: { + usd: number; + }; + price_change_percentage_7d_in_currency: { + usd: number; + }; + price_change_percentage_14d_in_currency: { + usd: number; + }; + price_change_percentage_30d_in_currency: { + usd: number; + }; + price_change_percentage_60d_in_currency: { + usd: number; + }; + price_change_percentage_200d_in_currency: { + usd: number; + }; + price_change_percentage_1y_in_currency: { + usd: number; + }; + max_supply: number; circulating_supply: number; total_supply: number; - max_supply: number; - ath: number; - ath_change_percentage: number; - ath_date: string; - atlDance: number; - atl_change_percentage: number; - atl_date: string; - roi: { - times: number; - currency: string; - percentage: number; - } | null; - last_updated: string; - sparkline_in_7d: { + sparkline_7d: { price: number[]; }; - price_change_percentage_1h_in_currency: number; - price_change_percentage_24h_in_currency: number; - price_change_percentage_7d_in_currency: number; - }; - - export type CoinDetail = { - id: string; - symbol: string; - name: string; - description: { - en: string; + ath: { + usd: number; }; - image: { - thumb: string; - small: string; - large: string; + ath_change_percentage: { + usd: number; }; - market_cap_rank: number; - links: { - homepage: string[]; - blockchain_site: string[]; - official_forum_url: string[]; - chat_url: string[]; - announcement_url: string[]; - twitter_screen_name: string; - facebook_username: string; - bitcointalk_thread_identifier: number | null; - telegram_channel_identifier: string; - subreddit_url: string; - repos_url: { - github: string[]; - bitbucket: string[]; - }; - }; - market_data: { - current_price: { - usd: number; - }; - market_cap: { - usd: number; - }; - market_cap_rank: number; - fully_diluted_valuation: { - usd: number; - }; - total_volume: { - usd: number; - }; - high_24h: { - usd: number; - }; - low_24h: { - usd: number; - }; - price_change_24h: number; - price_change_percentage_24h: number; - price_change_percentage_7d: number; - price_change_percentage_14d: number; - price_change_percentage_30d: number; - price_change_percentage_60d: number; - price_change_percentage_200d: number; - price_change_percentage_1y: number; - market_cap_change_24h: number; - market_cap_change_percentage_24h: number; - price_change_24h_in_currency: { - usd: number; - }; - price_change_percentage_1h_in_currency: { - usd: number; - }; - price_change_percentage_24h_in_currency: { - usd: number; - }; - price_change_percentage_7d_in_currency: { - usd: number; - }; - price_change_percentage_14d_in_currency: { - usd: number; - }; - price_change_percentage_30d_in_currency: { - usd: number; - }; - price_change_percentage_60d_in_currency: { - usd: number; - }; - price_change_percentage_200d_in_currency: { - usd: number; - }; - price_change_percentage_1y_in_currency: { - usd: number; - }; - max_supply: number; - circulating_supply: number; - total_supply: number; - sparkline_7d: { - price: number[]; - }; - ath: { - usd: number; - }; - ath_change_percentage: { - usd: number; - }; - ath_date: { - usd: string; - }; - atl: { - usd: number; - }; - atl_change_percentage: { - usd: number; - }; - atl_date: { - usd: string; - }; + ath_date: { + usd: string; }; -}; - - - - export type GlobalData = { - active_cryptocurrencies: number; - upcoming_icos: number; - ongoing_icos: number; - ended_icos: number; - markets: number; - total_market_cap: { - [key: string]: number; + atl: { + usd: number; }; - total_volume: { - [key: string]: number; + atl_change_percentage: { + usd: number; }; - market_cap_percentage: { - [key: string]: number; + atl_date: { + usd: string; }; - market_cap_change_percentage_24h_usd: number; - updated_at: number; }; - - export type CoinHistory = { - prices: [number, number][]; - market_caps: [number, number][]; - total_volumes: [number, number][]; - }; \ No newline at end of file +}; + +export type GlobalData = { + active_cryptocurrencies: number; + upcoming_icos: number; + ongoing_icos: number; + ended_icos: number; + markets: number; + total_market_cap: { + [key: string]: number; + }; + total_volume: { + [key: string]: number; + }; + market_cap_percentage: { + [key: string]: number; + }; + market_cap_change_percentage_24h_usd: number; + updated_at: number; +}; + +export type CoinHistory = { + prices: [number, number][]; + market_caps: [number, number][]; + total_volumes: [number, number][]; +}; \ No newline at end of file From eff332e894fb694575a01c1d70d7580c2c3861e8 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 23:32:29 +0700 Subject: [PATCH 037/107] Add client-side polyfills for crypto and other modules in webpack configuration --- app/transactions/page.tsx | 6 +- next.config.js | 31 +++++ next.config.js.backup | 31 +++++ package-lock.json | 232 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 next.config.js create mode 100644 next.config.js.backup diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 3ed1011..a13879b 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -31,7 +31,7 @@ export default function TransactionExplorer() { return (
- +
{/* Revenue Graph */} @@ -54,4 +54,6 @@ export default function TransactionExplorer() {
-
\ No newline at end of file +
+ ); +} \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..4c3b9dc --- /dev/null +++ b/next.config.js @@ -0,0 +1,31 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack: (config, { isServer }) => { + if (!isServer) { + // Client-side polyfills + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + path: require.resolve('path-browserify'), + buffer: require.resolve('buffer/'), + fs: false, + net: false, + tls: false, + http: false, + https: false, + zlib: false + } + } + + // Ignore native module build errors + config.resolve.alias = { + ...config.resolve.alias, + './build/Release/ecdh': false, + } + + return config + }, + } + + module.exports = nextConfig \ No newline at end of file diff --git a/next.config.js.backup b/next.config.js.backup new file mode 100644 index 0000000..c0ee7f5 --- /dev/null +++ b/next.config.js.backup @@ -0,0 +1,31 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack: (config, { isServer }) => { + if (!isServer) { + // Client-side polyfills + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + path: require.resolve('path-browserify'), + buffer: require.resolve('buffer/'), + fs: false, + net: false, + tls: false, + http: false, + https: false, + zlib: false + } + } + + // Ignore native module build errors + config.resolve.alias = { + ...config.resolve.alias, + './build/Release/ecdh': false, + } + + return config + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 880e823..2a36896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@safe-global/safe-apps-sdk": "^8.1.0", "@supabase/supabase-js": "^2.49.1", "@tanstack/react-query": "^5.67.1", + "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -52,11 +53,14 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", + "buffer": "^6.0.3", "bcrypt": "^5.1.1", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "crypto-browserify": "^3.12.1", + "dotenv": "^16.4.7", "crypto-js": "^4.2.0", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", @@ -99,7 +103,9 @@ "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", + "path-browserify": "^1.0.1", "postcss": "^8", + "stream-browserify": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -5738,6 +5744,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz", + "integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz", + "integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@trezor/analytics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.3.0.tgz", @@ -8908,6 +8941,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -9871,6 +9921,12 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -9883,6 +9939,22 @@ "node": ">=0.8" } }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -9942,6 +10014,45 @@ "uncrypto": "^0.1.3" } }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/crypto-browserify/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crypto-es": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/crypto-es/-/crypto-es-1.2.7.tgz", @@ -10492,6 +10603,23 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -10532,6 +10660,18 @@ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -14949,6 +15089,25 @@ "node": ">=8.6" } }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -15360,6 +15519,36 @@ "node": ">=6" } }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15780,6 +15969,13 @@ "integrity": "sha512-8e0JIqkRbMMPlFBnF9f+92hX1s07jdkd3tqB8uHE9L+cwGGjIYjQM7QLgt0FQ5MZp6SFFYYDm/Y48pqK3ZvJOQ==", "license": "MIT" }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -16160,6 +16356,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", @@ -16219,6 +16421,26 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -16325,6 +16547,16 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", From 466721c26f82ea426b050b10eff136ac43399ee8 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Thu, 13 Mar 2025 23:33:49 +0700 Subject: [PATCH 038/107] Add new dependencies for virtual scrolling and browser compatibility --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 2e914ce..50b4e88 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@safe-global/safe-apps-sdk": "^8.1.0", "@supabase/supabase-js": "^2.49.1", "@tanstack/react-query": "^5.67.1", + "@tanstack/react-virtual": "^3.13.2", "@web3-onboard/coinbase": "^2.4.2", "@web3-onboard/dcent": "^2.2.10", "@web3-onboard/frontier": "^2.1.1", @@ -53,11 +54,14 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", + "buffer": "^6.0.3", "bcrypt": "^5.1.1", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", + "crypto-browserify": "^3.12.1", + "dotenv": "^16.4.7", "crypto-js": "^4.2.0", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", @@ -100,7 +104,9 @@ "@types/react-router-dom": "^5.3.3", "eslint": "^9", "eslint-config-next": "15.1.6", + "path-browserify": "^1.0.1", "postcss": "^8", + "stream-browserify": "^3.0.0", "tailwindcss": "^3.4.1", "typescript": "^5" } From 806b994b74ce9dbc79305ec12a8d3a21ecdc3731 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:34:03 +0700 Subject: [PATCH 039/107] Update package dependencies and restore missing modules for improved functionality --- package-lock.json | 187 ++++++++++++++++++++++++++++++++++++++-------- package.json | 4 +- 2 files changed, 157 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a36896..eb28d2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,15 +53,15 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", - "buffer": "^6.0.3", "bcrypt": "^5.1.1", "bcryptjs": "^3.0.2", + "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", "crypto-browserify": "^3.12.1", - "dotenv": "^16.4.7", "crypto-js": "^4.2.0", + "dotenv": "^16.4.7", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", @@ -9368,6 +9368,119 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-sign/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -10570,6 +10683,16 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destr": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", @@ -15519,36 +15642,6 @@ "node": ">=6" } }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "license": "ISC", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-asn1/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15957,6 +16050,36 @@ "node": ">=6" } }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse-headers": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", diff --git a/package.json b/package.json index 50b4e88..b50e6f5 100644 --- a/package.json +++ b/package.json @@ -54,15 +54,15 @@ "@web3-onboard/walletconnect": "^2.6.2", "aos": "^2.3.4", "axios": "^1.7.9", - "buffer": "^6.0.3", "bcrypt": "^5.1.1", "bcryptjs": "^3.0.2", + "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", "crypto-browserify": "^3.12.1", - "dotenv": "^16.4.7", "crypto-js": "^4.2.0", + "dotenv": "^16.4.7", "eccrypto": "^1.1.6", "embla-carousel-react": "^8.5.2", "eth-crypto": "^2.7.0", From 1f1bc3c3dd81949d3489e3fb60a5442caca2ab22 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:37:38 +0700 Subject: [PATCH 040/107] Update layout metadata for CryptoPath with SEO enhancements and social media previews --- app/layout.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/layout.tsx b/app/layout.tsx index fe60ab4..8a451f8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -39,7 +39,29 @@ const geistMono = Geist_Mono({ * [existing comment preserved] */ export const metadata: Metadata = { - // [existing metadata preserved] + title: "CryptoPath", + description: "Create by members of group 3 - Navigate the world of blockchain with CryptoPath", + icons: { + icon: "/favicon.ico", + }, + openGraph: { + title: "CryptoPath", + description: "Create by members of group 3 - Navigate the world of blockchain with CryptoPath", + images: [ + { + url: '/og-image.jpg', + width: 1200, + height: 630, + alt: 'CryptoPath - Blockchain Explorer', + } + ], + }, + twitter: { + card: 'summary_large_image', + title: "CryptoPath", + description: "Create by members of group 3 - Navigate the world of blockchain with CryptoPath", + images: ['/og-image.jpg'], + }, }; /** From 05182af6c4641fe6082a2b687813da580ef5597c Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:42:51 +0700 Subject: [PATCH 041/107] Add remote image patterns and update webpack configuration for client-side polyfills --- next.config.js | 71 +++++++++++++++++++++++++++++++------------------- next.config.ts | 9 +++++++ 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/next.config.js b/next.config.js index 4c3b9dc..5f841a0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,31 +1,48 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - webpack: (config, { isServer }) => { - if (!isServer) { - // Client-side polyfills - config.resolve.fallback = { - ...config.resolve.fallback, - crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify'), - path: require.resolve('path-browserify'), - buffer: require.resolve('buffer/'), - fs: false, - net: false, - tls: false, - http: false, - https: false, - zlib: false - } + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "api.opensea.io", + pathname: "/api/v1/asset/**", + }, + { + protocol: "https", + hostname: "gateway.pinata.cloud", + pathname: "/ipfs/**", } - - // Ignore native module build errors - config.resolve.alias = { - ...config.resolve.alias, - './build/Release/ecdh': false, + ], + }, + env: { + ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY, + }, + webpack: (config, { isServer }) => { + if (!isServer) { + // Client-side polyfills + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + path: require.resolve('path-browserify'), + buffer: require.resolve('buffer/'), + fs: false, + net: false, + tls: false, + http: false, + https: false, + zlib: false } - - return config - }, - } - - module.exports = nextConfig \ No newline at end of file + } + + // Ignore native module build errors + config.resolve.alias = { + ...config.resolve.alias, + './build/Release/ecdh': false, + } + + return config + }, +} + +module.exports = nextConfig diff --git a/next.config.ts b/next.config.ts index d13b44a..6d0170c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -19,6 +19,15 @@ const nextConfig: NextConfig = { env: { ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY, }, + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve('crypto-browserify') + }; + } + return config; + } }; export default nextConfig; \ No newline at end of file From d18e9d7b833adc10128312645e917f962e0606c7 Mon Sep 17 00:00:00 2001 From: HungPhan-0612 <163500971+HungPhan-0612@users.noreply.github.com> Date: Fri, 14 Mar 2025 07:57:22 +0700 Subject: [PATCH 042/107] Refactor component imports and restructure Search,Search-offchain component for improved organization --- app/search-offchain/TransactionContent.tsx | 10 +++++----- app/search/TransactionContent.tsx | 12 ++++++------ .../{ => search-offchain}/SearchBarOffChain.tsx | 0 .../TransactionGraphOffChain.tsx | 0 .../TransactionTableOffChain.tsx | 0 components/{ => search}/NFTGallery.tsx | 0 components/{ => search}/Portfolio.tsx | 0 components/{ => search}/SearchBar.tsx | 0 components/{ => search}/TransactionGraph.tsx | 0 components/{ => search}/TransactionTable.tsx | 0 components/{ => search}/WalletInfo.tsx | 0 11 files changed, 11 insertions(+), 11 deletions(-) rename components/{ => search-offchain}/SearchBarOffChain.tsx (100%) rename components/{ => search-offchain}/TransactionGraphOffChain.tsx (100%) rename components/{ => search-offchain}/TransactionTableOffChain.tsx (100%) rename components/{ => search}/NFTGallery.tsx (100%) rename components/{ => search}/Portfolio.tsx (100%) rename components/{ => search}/SearchBar.tsx (100%) rename components/{ => search}/TransactionGraph.tsx (100%) rename components/{ => search}/TransactionTable.tsx (100%) rename components/{ => search}/WalletInfo.tsx (100%) diff --git a/app/search-offchain/TransactionContent.tsx b/app/search-offchain/TransactionContent.tsx index 1c30afb..b1136f8 100644 --- a/app/search-offchain/TransactionContent.tsx +++ b/app/search-offchain/TransactionContent.tsx @@ -1,11 +1,11 @@ 'use client' -import WalletInfo from "@/components/WalletInfo" -import TransactionGraphOffChain from "@/components/TransactionGraphOffChain" -import TransactionTableOffChain from "@/components/TransactionTableOffChain" -import Portfolio from "@/components/Portfolio" +import WalletInfo from "@/components/search/WalletInfo" +import TransactionGraphOffChain from "@/components/search-offchain/TransactionGraphOffChain" +import TransactionTableOffChain from "@/components/search-offchain/TransactionTableOffChain" +import Portfolio from "@/components/search/Portfolio" import { useSearchParams } from "next/navigation" -import SearchBarOffChain from "@/components/SearchBarOffChain" +import SearchBarOffChain from "@/components/search-offchain/SearchBarOffChain" export default function Transactions() { diff --git a/app/search/TransactionContent.tsx b/app/search/TransactionContent.tsx index 499b8a5..6fa8520 100644 --- a/app/search/TransactionContent.tsx +++ b/app/search/TransactionContent.tsx @@ -1,11 +1,11 @@ 'use client' -import SearchBar from "@/components/SearchBar" -import WalletInfo from "@/components/WalletInfo" -import TransactionGraph from "@/components/TransactionGraph" -import TransactionTable from "@/components/TransactionTable" -import Portfolio from "@/components/Portfolio" -import NFTGallery from "@/components/NFTGallery" +import SearchBar from "@/components/search/SearchBar" +import WalletInfo from "@/components/search/WalletInfo" +import TransactionGraph from "@/components/search/TransactionGraph" +import TransactionTable from "@/components/search/TransactionTable" +import Portfolio from "@/components/search/Portfolio" +import NFTGallery from "@/components/search/NFTGallery" import { useSearchParams } from "next/navigation" diff --git a/components/SearchBarOffChain.tsx b/components/search-offchain/SearchBarOffChain.tsx similarity index 100% rename from components/SearchBarOffChain.tsx rename to components/search-offchain/SearchBarOffChain.tsx diff --git a/components/TransactionGraphOffChain.tsx b/components/search-offchain/TransactionGraphOffChain.tsx similarity index 100% rename from components/TransactionGraphOffChain.tsx rename to components/search-offchain/TransactionGraphOffChain.tsx diff --git a/components/TransactionTableOffChain.tsx b/components/search-offchain/TransactionTableOffChain.tsx similarity index 100% rename from components/TransactionTableOffChain.tsx rename to components/search-offchain/TransactionTableOffChain.tsx diff --git a/components/NFTGallery.tsx b/components/search/NFTGallery.tsx similarity index 100% rename from components/NFTGallery.tsx rename to components/search/NFTGallery.tsx diff --git a/components/Portfolio.tsx b/components/search/Portfolio.tsx similarity index 100% rename from components/Portfolio.tsx rename to components/search/Portfolio.tsx diff --git a/components/SearchBar.tsx b/components/search/SearchBar.tsx similarity index 100% rename from components/SearchBar.tsx rename to components/search/SearchBar.tsx diff --git a/components/TransactionGraph.tsx b/components/search/TransactionGraph.tsx similarity index 100% rename from components/TransactionGraph.tsx rename to components/search/TransactionGraph.tsx diff --git a/components/TransactionTable.tsx b/components/search/TransactionTable.tsx similarity index 100% rename from components/TransactionTable.tsx rename to components/search/TransactionTable.tsx diff --git a/components/WalletInfo.tsx b/components/search/WalletInfo.tsx similarity index 100% rename from components/WalletInfo.tsx rename to components/search/WalletInfo.tsx From 108b0cccf58f6647836ac60c4ff5b83c2ac18f00 Mon Sep 17 00:00:00 2001 From: DangDuyLe Date: Fri, 14 Mar 2025 09:52:18 +0700 Subject: [PATCH 043/107] Refactor RevenueGraph and WalletCharts components to fetch and display cryptocurrency data dynamically. --- components/transactions/RevenueGraph.tsx | 274 +++++++++---- components/transactions/WalletCharts.tsx | 466 +++++++++++------------ services/cryptoService.ts | 166 ++++++++ 3 files changed, 599 insertions(+), 307 deletions(-) create mode 100644 services/cryptoService.ts diff --git a/components/transactions/RevenueGraph.tsx b/components/transactions/RevenueGraph.tsx index 5093401..fe7a4e7 100644 --- a/components/transactions/RevenueGraph.tsx +++ b/components/transactions/RevenueGraph.tsx @@ -1,85 +1,221 @@ 'use client'; +import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +import { fetchHistoricalData, fetchAvailableCoins, CryptoMarketData, CoinOption } from "@/services/cryptoService"; +import { Loader2, AlertCircle, RefreshCcw } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; -const data = [ - { month: 'Jan', revenue2024: 15, revenue2023: -15 }, - { month: 'Feb', revenue2024: 5, revenue2023: -18 }, - { month: 'Mar', revenue2024: 12, revenue2023: -10 }, - { month: 'Apr', revenue2024: 25, revenue2023: -15 }, - { month: 'May', revenue2024: 15, revenue2023: -5 }, - { month: 'Jun', revenue2024: 10, revenue2023: -17 }, - { month: 'Jul', revenue2024: 7, revenue2023: -15 }, - { month: 'Aug', revenue2024: 15, revenue2023: -5 }, - { month: 'Sep', revenue2024: 10, revenue2023: -17 }, - { month: 'Oct', revenue2024: 7, revenue2023: -15 }, - { month: 'Nov', revenue2024: 15, revenue2023: -5 }, - { month: 'Dec', revenue2024: 20, revenue2023: -17 }, -]; +interface ChartData { + date: string; + price: number; + volume: number; +} export default function RevenueGraph() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [selectedCoin, setSelectedCoin] = useState({ + id: 'ethereum', + symbol: 'ETH', + name: 'Ethereum' + }); + const [availableCoins, setAvailableCoins] = useState([]); + const [loadingCoins, setLoadingCoins] = useState(true); + + // Fetch available coins + useEffect(() => { + const fetchCoins = async () => { + try { + setLoadingCoins(true); + const coins = await fetchAvailableCoins(); + setAvailableCoins(coins); + } catch (err) { + console.error('Error fetching available coins:', err); + } finally { + setLoadingCoins(false); + } + }; + + fetchCoins(); + }, []); + + // Fetch data for selected coin + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + const coinData = await fetchHistoricalData(selectedCoin.id, 30); + + // Process the data + const chartData: ChartData[] = coinData.prices.map((price, index) => { + const date = new Date(price[0]); + return { + date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + price: price[1], + volume: coinData.total_volumes[index][1] / 1000000 // Convert to millions + }; + }); + + setData(chartData); + } catch (err) { + setError('Failed to fetch data. Please try again.'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedCoin, retryCount]); + + const handleCoinChange = (coinId: string) => { + const coin = availableCoins.find(c => c.id === coinId); + if (coin) { + setSelectedCoin(coin); + } + }; + + const handleRetry = () => { + setRetryCount(prev => prev + 1); + }; + + const LoadingState = () => ( +
+ +

Loading {selectedCoin.name} data...

+
+ ); + + const ErrorState = () => ( +
+ +

{error}

+ +
+ ); + return ( -
- Total Revenue -
-
-
- 2024 -
-
-
- 2023 -
-
+
+ + {selectedCoin.name} Price & Volume + +
-
- - - - `${value}`} - /> - - - - - -
+ {loading ? ( + + ) : error ? ( + + ) : ( +
+ + + + `$${value.toLocaleString()}`} + /> + `${value.toFixed(0)}M`} + /> + [ + name === 'price' ? `$${value.toLocaleString()}` : `${value.toFixed(0)}M`, + name === 'price' ? 'Price' : 'Volume' + ]} + /> + + + + +
+ )}
); -} \ No newline at end of file +} diff --git a/components/transactions/WalletCharts.tsx b/components/transactions/WalletCharts.tsx index 7a63dcd..daf871c 100644 --- a/components/transactions/WalletCharts.tsx +++ b/components/transactions/WalletCharts.tsx @@ -1,209 +1,204 @@ 'use client'; +import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Line, LineChart, BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Area, AreaChart, PieChart, Pie, Cell } from "recharts"; -import { BarChart3, Gauge, Wallet, History, CheckCircle2, Network } from "lucide-react"; +import { Line, LineChart, BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Area, AreaChart, PieChart, Pie, Cell, RadialBarChart, RadialBar } from "recharts"; +import { Blocks, Activity, Gauge, Wallet, TrendingUp, Clock, Database, Cpu, Shield, CheckCircle } from "lucide-react"; +import { fetchBlockchainMetrics, fetchGlobalMetrics, BlockchainMetrics } from "@/services/cryptoService"; +import { Loader2 } from "lucide-react"; -const transactionTypeData = [ - { week: 'W1', defi: 450, nft: 320, swap: 230 }, - { week: 'W2', defi: 520, nft: 280, swap: 310 }, - { week: 'W3', defi: 710, nft: 420, swap: 380 }, - { week: 'W4', defi: 480, nft: 350, swap: 290 }, - { week: 'W5', defi: 520, nft: 390, swap: 420 }, - { week: 'W6', defi: 630, nft: 450, swap: 380 }, -]; +interface GlobalMetrics { + total_market_cap: { usd: number }; + total_volume: { usd: number }; + market_cap_percentage: { [key: string]: number }; + active_cryptocurrencies: number; + markets: number; +} -const gasUsageData = [ - { name: 'Smart Contracts', usage: 45, percentage: '45%' }, - { name: 'Token Transfers', usage: 30, percentage: '30%' }, - { name: 'NFT Trading', usage: 15, percentage: '15%' }, - { name: 'Other', usage: 10, percentage: '10%' }, -]; - -const miniChartData = { - tokenHoldings: [ - { name: 'ETH', value: 60 }, - { name: 'USDT', value: 25 }, - { name: 'Other', value: 15 }, - ], - walletAge: [ - { date: '1', value: 10 }, - { date: '2', value: 15 }, - { date: '3', value: 12 }, - { date: '4', value: 18 }, - { date: '5', value: 22 }, - { date: '6', value: 20 }, - ], - transactionSuccess: [ - { name: 'Success', value: 85 }, - { name: 'Failed', value: 15 }, - ], - networkInteractions: [ - { name: 'DeFi Protocols', value: 40 }, - { name: 'DEX', value: 30 }, - { name: 'NFT Markets', value: 20 }, - { name: 'Others', value: 10 }, - ], +const COLORS = { + primary: '#F5B056', + secondary: '#3b82f6', + tertiary: '#22c55e', + quaternary: '#a855f7', + error: '#ef4444', + gray: '#666', }; -const COLORS = ['#F5B056', '#a855f7', '#22c55e', '#666']; - export default function WalletCharts() { - const tooltipStyle = { - backgroundColor: '#1f2937', - border: 'none', - borderRadius: '8px', - color: '#fff' + const [blockchainMetrics, setBlockchainMetrics] = useState(null); + const [globalMetrics, setGlobalMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const [blockchain, global] = await Promise.all([ + fetchBlockchainMetrics(), + fetchGlobalMetrics() + ]); + setBlockchainMetrics(blockchain); + setGlobalMetrics(global); + } catch (err) { + setError('Failed to fetch metrics'); + console.error('Error fetching metrics:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + // Refresh data every 30 seconds + const interval = setInterval(fetchData, 30000); + return () => clearInterval(interval); + }, []); + + if (loading) { + return ( +
+
+ {[1, 2].map((i) => ( + + + + + + ))} +
+
+ ); + } + + if (error || !blockchainMetrics || !globalMetrics) { + return ( +
+ Error loading metrics. Please try again later. +
+ ); + } + + const formatBlockNumber = (num: number): string => { + if (isNaN(num)) return '0'; + return num.toLocaleString(); + }; + + const formatBlocksBehind = (latest: number, current: number): string => { + const behind = latest - current; + if (isNaN(behind)) return '0'; + return behind.toLocaleString(); }; - const tooltipFormatter = (value: number, name: string) => { - return [ - `${value}%`, - `${name}`, - ]; + const formatNumber = (num: number): string => { + if (num >= 1e12) return `${(num / 1e12).toFixed(2)}T`; + if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`; + if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`; + if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`; + return num.toFixed(2); }; + // Prepare data for visualizations + const blockData = [ + { name: 'Block Height', lastBlock: blockchainMetrics.lastBlock, safeBlock: blockchainMetrics.safeBlock, finalizedBlock: blockchainMetrics.finalizedBlock } + ]; + + const networkHealthData = [ + { name: 'Block Time', value: (blockchainMetrics.avgBlockTime / 15) * 100, fill: COLORS.primary }, + { name: 'Gas Price', value: (blockchainMetrics.gasPrice / 50) * 100, fill: COLORS.secondary }, + { name: 'Validators', value: (blockchainMetrics.activeValidators / 600000) * 100, fill: COLORS.tertiary }, + { name: 'Staking', value: (blockchainMetrics.stakingAPR / 10) * 100, fill: COLORS.quaternary } + ]; + + const marketShareData = Object.entries(globalMetrics.market_cap_percentage || {}) + .slice(0, 5) + .map(([name, value], index) => ({ + name: name.toUpperCase(), + value, + fill: Object.values(COLORS)[index] + })); + return ( -
-
- {/* Transaction Types Overview */} +
+ {/* Block Heights Stats */} +
- -
-
- - Transaction Types -
-
-
-
- DeFi -
-
-
- NFT -
-
-
- Swap -
-
+ +
+ + Last Block
- - -
- - - - - - - - - - +
+ + #{formatBlockNumber(blockchainMetrics.lastBlock)} +
- {/* Gas Usage Distribution */} - -
-
- - Gas Usage Distribution + +
+ + Safe Block +
+
+
+ + #{formatBlockNumber(blockchainMetrics.safeBlock)} + + + {formatBlocksBehind(blockchainMetrics.lastBlock, blockchainMetrics.safeBlock)} blocks behind +
- - -
- {gasUsageData.map((item, index) => ( -
-
- {item.name} - {item.percentage} -
-
-
-
-
- ))} + + + + + +
+ + Finalized Block +
+
+
+ + #{formatBlockNumber(blockchainMetrics.finalizedBlock)} + + + {formatBlocksBehind(blockchainMetrics.lastBlock, blockchainMetrics.finalizedBlock)} blocks behind + +
-
- {/* Token Distribution */} + {/* Network Health and Market Stats */} +
+ {/* Market Share - Pie Chart */} - +
- - Token Distribution + + Market Share
- -
+ +
- {miniChartData.tokenHoldings.map((entry, index) => ( - + {marketShareData.map((entry, index) => ( + ))} - - - -
-
- - - {/* Wallet Age Activity */} - - -
- - Wallet Age Activity -
-
- -
- - - [`${value.toFixed(2)}%`]} /> - +
+
+ {marketShareData.map((entry) => ( +
+
+ {entry.name} +
+ ))} +
- {/* Transaction Success Rate */} - - -
-
- - Success Rate + {/* Market Stats Grid */} +
+ + +
+ + Market Cap
-
85%
-
- - -
- - - - - - - - - -
-
- + + +

+ ${formatNumber(globalMetrics.total_market_cap.usd)} +

+
+ - {/* Network Interactions */} - - -
- - Network Interactions -
-
- -
- - - - {miniChartData.networkInteractions.map((entry, index) => ( - - ))} - - - - -
-
-
+ + +
+ + 24h Volume +
+
+ +

+ ${formatNumber(globalMetrics.total_volume.usd)} +

+
+
+ + + +
+ + Active Coins +
+
+ +

+ {globalMetrics.active_cryptocurrencies.toLocaleString()} +

+
+
+ + + +
+ + Markets +
+
+ +

+ {globalMetrics.markets.toLocaleString()} +

+
+
+
); diff --git a/services/cryptoService.ts b/services/cryptoService.ts new file mode 100644 index 0000000..01e5ebb --- /dev/null +++ b/services/cryptoService.ts @@ -0,0 +1,166 @@ +// CoinGecko API service +const COINGECKO_API_BASE = 'https://api.coingecko.com/api/v3'; + +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +async function fetchWithRetry(url: string, retries = 3, delayMs = 1000): Promise { + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(url); + if (response.ok) return response; + + // If rate limited, wait longer + if (response.status === 429) { + await delay(delayMs * 2); + continue; + } + + throw new Error(`HTTP error! status: ${response.status}`); + } catch (error) { + if (i === retries - 1) throw error; + await delay(delayMs); + } + } + throw new Error('Max retries reached'); +} + +export interface CryptoMarketData { + prices: [number, number][]; // [timestamp, price] + market_caps: [number, number][]; + total_volumes: [number, number][]; +} + +export interface TokenData { + id: string; + symbol: string; + name: string; + current_price: number; + market_cap: number; + total_volume: number; + price_change_percentage_24h: number; +} + +export interface CoinOption { + id: string; + symbol: string; + name: string; +} + +export interface BlockchainMetrics { + lastBlock: number; + safeBlock: number; + finalizedBlock: number; + avgBlockTime: number; + gasPrice: number; + activeValidators: number; + stakingAPR: number; +} + +export interface GlobalMetrics { + total_market_cap: { usd: number }; + total_volume: { usd: number }; + market_cap_percentage: { [key: string]: number }; + active_cryptocurrencies: number; + markets: number; +} + +export const fetchAvailableCoins = async (): Promise => { + try { + const response = await fetchWithRetry( + `${COINGECKO_API_BASE}/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1&sparkline=false` + ); + const data = await response.json(); + return data.map((coin: any) => ({ + id: coin.id, + symbol: coin.symbol.toUpperCase(), + name: coin.name + })); + } catch (error) { + console.error('Error fetching available coins:', error); + throw error; + } +}; + +export const fetchHistoricalData = async (coinId: string, days: number = 30, currency: string = 'usd'): Promise => { + try { + const response = await fetchWithRetry( + `${COINGECKO_API_BASE}/coins/${coinId}/market_chart?vs_currency=${currency}&days=${days}&interval=daily` + ); + return await response.json(); + } catch (error) { + console.error('Error fetching historical data:', error); + throw error; + } +}; + +export const fetchTopTokens = async (limit: number = 10, currency: string = 'usd'): Promise => { + try { + const response = await fetchWithRetry( + `${COINGECKO_API_BASE}/coins/markets?vs_currency=${currency}&order=market_cap_desc&per_page=${limit}&page=1&sparkline=false` + ); + return await response.json(); + } catch (error) { + console.error('Error fetching top tokens:', error); + throw error; + } +}; + +export const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); +}; + +export const formatPercentage = (value: number): string => { + return `${value.toFixed(2)}%`; +}; + +const ETHERSCAN_BASE_URL = 'https://api.etherscan.io/api'; + +export const fetchBlockchainMetrics = async (): Promise => { + // Since we don't have an API key, we'll use simulated data that updates + const baseBlock = Math.floor(Date.now() / 12000) + 19000000; // Simulates new blocks every ~12 seconds + + return { + lastBlock: baseBlock, + safeBlock: baseBlock - 64, // ~13 minutes behind + finalizedBlock: baseBlock - 128, // ~26 minutes behind + avgBlockTime: 12, + gasPrice: 25, + activeValidators: 889643, + stakingAPR: 3.7 + }; +}; + +export const fetchGlobalMetrics = async (): Promise => { + try { + const response = await fetch('https://api.coingecko.com/api/v3/global'); + const data = await response.json(); + return { + total_market_cap: { usd: data.data.total_market_cap.usd }, + total_volume: { usd: data.data.total_volume.usd }, + market_cap_percentage: data.data.market_cap_percentage, + active_cryptocurrencies: data.data.active_cryptocurrencies, + markets: data.data.markets + }; + } catch (error) { + console.error('Error fetching global metrics:', error); + // Return fallback data if API fails + return { + total_market_cap: { usd: 2785000000000 }, + total_volume: { usd: 89700000000 }, + market_cap_percentage: { + btc: 51.2, + eth: 16.8, + usdt: 7.3, + bnb: 4.1, + xrp: 3.2 + }, + active_cryptocurrencies: 11250, + markets: 914 + }; + } +}; \ No newline at end of file From 8224e7ad8a13f169a0b8f6e9d71499bafc8ef0a4 Mon Sep 17 00:00:00 2001 From: HungPhan-0612 <163500971+HungPhan-0612@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:08:37 +0700 Subject: [PATCH 044/107] Refactor button and input components to use consistent rounded styles Header, Footer, HomePage, add EthPriceLine --- app/globals.css | 20 ++--- app/page.tsx | 39 +++++----- components/EthPriceLine.tsx | 141 ++++++++++++++++++++++++++++++++++++ components/Footer.tsx | 4 +- components/Header.tsx | 46 ++++++------ 5 files changed, 196 insertions(+), 54 deletions(-) create mode 100644 components/EthPriceLine.tsx diff --git a/app/globals.css b/app/globals.css index 9caeb36..2378449 100644 --- a/app/globals.css +++ b/app/globals.css @@ -59,7 +59,7 @@ * Button System * ====================================== */ .cp-button { - @apply px-6 py-3 rounded-lg font-semibold transition cursor-pointer; + @apply px-6 py-3 rounded-[5px] font-semibold transition cursor-pointer; } .cp-button--primary { @@ -71,16 +71,16 @@ } .cp-button--rounded { - @apply rounded-full; + @apply rounded-[5px]; } /* Legacy button styles - consider migrating to cp-button system */ .btn { - @apply py-3 px-6 border border-white rounded-md font-semibold cursor-pointer transition-all duration-200; + @apply py-3 px-6 border border-white rounded-[5px] font-semibold cursor-pointer transition-all duration-200; } .btn:hover { - @apply rounded-[0.9rem]; + @apply rounded-[5px]; } .btn:active { @@ -99,7 +99,7 @@ * Card System * ====================================== */ .cp-card { - @apply rounded-lg shadow-md overflow-hidden; + @apply rounded-[5px] shadow-md overflow-hidden; } .cp-card--dark { @@ -130,7 +130,7 @@ } .cp-input { - @apply px-4 py-2 rounded-md focus:outline-none; + @apply px-4 py-2 rounded-[5px] focus:outline-none; } .cp-input--dark { @@ -141,12 +141,12 @@ * Media Components * ====================================== */ .cp-video-container { - @apply bg-[#2d2d2d] rounded-md overflow-hidden relative w-full max-w-[800px] mx-auto; + @apply bg-[#2d2d2d] rounded-[5px] overflow-hidden relative w-full max-w-[800px] mx-auto; } /* Legacy video container - consider migrating to cp-video-container */ .video-container { - @apply bg-[#2d2d2d] rounded-md overflow-hidden relative w-full max-w-[800px] mx-auto; + @apply bg-[#2d2d2d] rounded-[5px] overflow-hidden relative w-full max-w-[800px] mx-auto; } @media screen and (max-width: 768px) { @@ -195,12 +195,12 @@ * Partner Components * ====================================== */ .cp-trusted-partner { - @apply bg-white p-4 rounded-md shadow-md text-center; + @apply bg-white p-4 rounded-[5px] shadow-md text-center; } /* Legacy trusted logo styles - consider migrating to cp-trusted-partner */ .trusted-logo { - @apply bg-white p-4 rounded-md shadow-md text-center; + @apply bg-white p-4 rounded-[5px] shadow-md text-center; } /* ====================================== diff --git a/app/page.tsx b/app/page.tsx index d8c6a5e..88ed8b5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'; import Image from 'next/image'; import { FaFacebookF, FaGithub, FaLinkedinIn } from 'react-icons/fa'; import ParticlesBackground from '@/components/ParticlesBackground'; +import EthPriceLine from '@/components/EthPriceLine'; import FAQ from './FAQ'; import AOS from 'aos'; import 'aos/dist/aos.css'; @@ -236,7 +237,7 @@ const HomePage = () => { return (
- +
{/* Description Section */}
@@ -257,7 +258,7 @@ const HomePage = () => { value={email} onChange={handleEmailChange} disabled={isSubmitting} - className={`px-4 py-3 w-full md:w-64 rounded-md bg-gray-900 border ${ + className={`px-4 py-3 w-full md:w-64 rounded-[5px] bg-gray-900 border ${ emailError ? 'border-red-500' : isSuccess ? 'border-green-500' : 'border-gray-700' } text-white focus:outline-none transition-colors`} /> @@ -297,9 +298,9 @@ const HomePage = () => {

-
+
-
+
@@ -332,7 +333,7 @@ const HomePage = () => {

{/* Video 1: YouTube Embed */} -
+
-
-

{t.whatIsCryptocurrency}

-

{t.explainingNewCurrency}

-
- - {/* Video 2: YouTube Embed */} -
- -
-

{t.redefiningSystem}

-

{t.welcomeToWeb3}

+ ))} +
+
+ + + {/* FAQ Section */} + + + {/* CTA Section */} +
+
+
+
+
+

{t.readyToStart}

+

+ {t.joinThousands} +

-
- - {/* Video 3: YouTube Embed */} -
- -
-

{t.whatIsBlockchain}

-

{t.understandBlockchain}

+ +
+ + + + + +
+
+
+ ); +}; - {/* Trusted Leaders Section */} -
-
-

- {t.trustedBy} {t.industryLeaders} -

-
-
-
- Facebook -

Facebook

-
-
- Apple -

Apple

-
-
- Amazon -

Amazon

-
-
- Netflix -

Netflix

-
-
- Google -

Google

-
-
- -
-
- Minh Duy Nguyen -
-
-

- {t.testimonialText} -

-

Nguyen Minh Duy

-

{t.founderOf}

-
+// FeatureCard component updated to use language prop +const FeatureCard = ({ icon, title, description, href, imageUrl, delay, language }: FeatureCardProps) => { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + +
+ {imageUrl && ( +
+
+
+ )}
-
- - {/* CTA Section (New) */} -
-
-

{t.readyToStart}

-

- {t.joinThousands} -

-
- - + + +
{icon}
+

{title}

+

{description}

+ +
+ {translations[language].explore} {/* Use language prop */} +
-
-
- - {/* Insert FAQ component here - Pass language to FAQ component */} - -
-
+ + + + ); }; -export default HomePage; \ No newline at end of file +export default LandingPage; \ No newline at end of file diff --git a/components/Header.tsx b/components/Header.tsx index 6e2e689..40d03fa 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -179,6 +179,9 @@ const Header = () => { Home + + Market Overview + PriceTable diff --git a/components/NFT/NFTNavigation.tsx b/components/NFT/NFTNavigation.tsx new file mode 100644 index 0000000..58bcf52 --- /dev/null +++ b/components/NFT/NFTNavigation.tsx @@ -0,0 +1,96 @@ + +import React from 'react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ArrowRight, BadgePercent, Grid3X3, Wallet, ExternalLink } from 'lucide-react'; + +interface NavCardProps { + icon: React.ReactNode; + title: string; + description: string; + href: string; + color: string; + delay: number; + isActive?: boolean; + image?: string; +} + +const NavCard = ({ icon, title, description, href, color, delay, isActive, image }: NavCardProps) => { + return ( + + + + {image && ( +
+ {title} +
+
+ )} + +
+ {icon} +
+

{title}

+

{description}

+
+ Explore + +
+
+
+ +
+ ); +}; + +export default function NFTNavigation({ currentPath }: { currentPath?: string }) { + return ( +
+ } + title="PATH NFT Marketplace" + description="Buy, sell, and create NFTs on the PATH token ecosystem. List your digital assets and trade with other users." + href="/NFT" + color="border-blue-500/30" + delay={0.1} + isActive={currentPath === '/NFT'} + image="/Img/Web3.webp" + /> + + } + title="NFT Collection Scanner" + description="Explore NFT collections across all EVM-based blockchains. Browse popular collections or connect your wallet to view your own NFTs." + href="/NFT/collection" + color="border-purple-500/30" + delay={0.2} + isActive={currentPath?.startsWith('/NFT/collection')} + image="/Img/Web3.webp" + /> + +
+
+
+

New to NFTs?

+

Learn how to connect your wallet and explore the world of NFTs

+
+ +
+
+
+ ); +} diff --git a/components/home/DemoShowcase.tsx b/components/home/DemoShowcase.tsx new file mode 100644 index 0000000..2473cfb --- /dev/null +++ b/components/home/DemoShowcase.tsx @@ -0,0 +1,182 @@ + +'use client'; +import React, { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import Image from 'next/image'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react'; + +const pageShowcases = [ + { + title: 'Crypto Price Dashboard', + description: 'Track real-time prices and market data across thousands of cryptocurrencies.', + image: '/Img/Exchange.webp', + path: '/pricetable', + color: 'from-blue-500 to-purple-600' + }, + { + title: 'Market Overview', + description: 'Get comprehensive insights into global crypto market metrics and trends.', + image: '/Img/market-overview.png', // Fixed path by removing 'public/' + path: '/market-overview', + color: 'from-green-500 to-teal-600' + }, + { + title: 'NFT Marketplace', + description: 'Buy, sell, and create unique digital assets on the PATH token ecosystem.', + image: '/Img/Web3.webp', + path: '/NFT', + color: 'from-purple-500 to-pink-600' + }, + { + title: 'NFT Collection Scanner', + description: 'Explore popular NFT collections or connect your wallet to browse your own NFTs.', + image: '/Img/Web3.webp', + path: '/NFT/collection', + color: 'from-indigo-500 to-blue-600' + }, + { + title: 'Transaction Explorer', + description: 'Search and analyze blockchain transactions with detailed visualizations.', + image: '/Img/Web3.webp', + path: '/search', + color: 'from-orange-500 to-red-600' + } +]; + +export default function DemoShowcase() { + const [activeIndex, setActiveIndex] = useState(0); + const [autoplay, setAutoplay] = useState(true); + + // Auto rotation for slides + useEffect(() => { + if (!autoplay) return; + + const interval = setInterval(() => { + setActiveIndex((prev) => (prev + 1) % pageShowcases.length); + }, 5000); + + return () => clearInterval(interval); + }, [autoplay]); + + const nextSlide = () => { + setAutoplay(false); + setActiveIndex((prev) => (prev + 1) % pageShowcases.length); + }; + + const prevSlide = () => { + setAutoplay(false); + setActiveIndex((prev) => (prev - 1 + pageShowcases.length) % pageShowcases.length); + }; + + const goToSlide = (index: number) => { + setAutoplay(false); + setActiveIndex(index); + }; + + return ( +
+
+ +

Explore Our Platform

+

+ See what CryptoPath has to offer with our comprehensive suite of blockchain tools +

+
+ +
+
+ {pageShowcases.map((showcase, index) => ( + +
+
+ {showcase.title} +
+
+ +
+
+

{showcase.title}

+ + {index + 1}/{pageShowcases.length} + +
+

{showcase.description}

+
+ + + + + View Demo + + +
+
+
+
+ ))} +
+ + + + + +
+ {pageShowcases.map((_, index) => ( +
+
+
+
+ ); +} diff --git a/components/home/FeatureCard.tsx b/components/home/FeatureCard.tsx new file mode 100644 index 0000000..e49a2c3 --- /dev/null +++ b/components/home/FeatureCard.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import Link from 'next/link'; +import { ArrowRight } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { motion } from 'framer-motion'; + +interface FeatureCardProps { + icon: React.ReactNode; + title: string; + description: string; + href: string; + imageUrl?: string; + delay: number; + language: 'en' | 'vi'; +} + +// Translation object +const translations = { + en: { + explore: 'Explore', + }, + vi: { + explore: 'Khám Phá', + }, +}; + +export default function FeatureCard({ + icon, + title, + description, + href, + imageUrl, + delay, + language +}: FeatureCardProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + +
+ {imageUrl && ( +
+
+
+ )} +
+ + +
{icon}
+

{title}

+

{description}

+ +
+ {translations[language].explore} + +
+
+ + + + ); +} \ No newline at end of file diff --git a/components/market-overview/AltcoinIndex.tsx b/components/market-overview/AltcoinIndex.tsx new file mode 100644 index 0000000..ee5adac --- /dev/null +++ b/components/market-overview/AltcoinIndex.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle } from 'lucide-react'; +import axios from 'axios'; + +interface AltcoinSeasonData { + value: number; + valueText: string; + btcDominance: number; + timestamp: number; + simulated?: boolean; +} + +export default function AltcoinIndex() { + const [altcoinIndex, setAltcoinIndex] = useState(50); + const [btcDominance, setBtcDominance] = useState(60); + const [isLoading, setIsLoading] = useState(true); + const [seasonText, setSeasonText] = useState("Neutral"); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchAltcoinSeasonData = async () => { + try { + const response = await axios.get('/api/market/altcoin-season'); + setAltcoinIndex(response.data.value); + setSeasonText(response.data.valueText); + setBtcDominance(response.data.btcDominance); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch Altcoin Season Index:", error); + // Generate a fallback value + const fallbackValue = Math.floor(Math.random() * 100) + 1; + setAltcoinIndex(fallbackValue); + setBtcDominance(60 + (Math.random() * 10 - 5)); + setIsSimulated(true); + + // Set season text based on value + if (fallbackValue <= 25) setSeasonText("Bitcoin Season"); + else if (fallbackValue < 45) setSeasonText("Bitcoin Favored"); + else if (fallbackValue < 55) setSeasonText("Neutral"); + else if (fallbackValue < 75) setSeasonText("Altcoin Favored"); + else setSeasonText("Altcoin Season"); + } finally { + setIsLoading(false); + } + }; + + fetchAltcoinSeasonData(); + }, []); + + // Determine season based on the altcoin index + const getSeason = (value: number): { text: string; color: string } => { + if (value <= 25) return { text: 'Bitcoin Season', color: 'from-orange-500 to-yellow-500' }; + if (value < 45) return { text: 'Bitcoin Favored', color: 'from-yellow-500 to-yellow-300' }; + if (value < 55) return { text: 'Neutral', color: 'from-blue-500 to-purple-500' }; + if (value < 75) return { text: 'Altcoin Favored', color: 'from-blue-500 to-blue-300' }; + return { text: 'Altcoin Season', color: 'from-blue-700 to-blue-500' }; + }; + + const season = getSeason(altcoinIndex); + + return ( + + + + + Altcoin Season Index + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : ( +
+
+
{altcoinIndex}/100
+
+ {seasonText} +
+
+ +
+
+
+ + Bitcoin Season + +
+
+ + Altcoin Season + +
+
+
+
+
+
+ 0 + 25 + 75 + 100 +
+ +
+
+ BTC Dominance: + {btcDominance.toFixed(1)}% +
+
+
+
+ )} +
+
+ ); +} diff --git a/components/market-overview/BlockchainStatsCard.tsx b/components/market-overview/BlockchainStatsCard.tsx new file mode 100644 index 0000000..49ad160 --- /dev/null +++ b/components/market-overview/BlockchainStatsCard.tsx @@ -0,0 +1,141 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, Database, Layers } from 'lucide-react'; +import axios from 'axios'; + +interface BlockchainStats { + hashRate: number; + difficulty: number; + latestHeight: number; + unconfirmedTx: number; + mempool: number; + btcMined: number; + marketPrice: number; + transactionRate: number; + minutesBetweenBlocks: number; + totalFees: number; +} + +interface ChainStatsData { + data: BlockchainStats; + timestamp: number; + simulated?: boolean; +} + +export default function BlockchainStatsCard() { + const [chainData, setChainData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchChainData = async () => { + try { + const response = await axios.get('/api/analytics/chain-stats'); + setChainData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch blockchain stats:", error); + } finally { + setIsLoading(false); + } + }; + + fetchChainData(); + + // Refresh every 5 minutes + const interval = setInterval(fetchChainData, 5 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const formatHashRate = (hashRate: number): string => { + if (hashRate >= 1000000000000000) return `${(hashRate / 1000000000000000).toFixed(2)} EH/s`; + if (hashRate >= 1000000000000) return `${(hashRate / 1000000000000).toFixed(2)} TH/s`; + if (hashRate >= 1000000000) return `${(hashRate / 1000000000).toFixed(2)} GH/s`; + if (hashRate >= 1000000) return `${(hashRate / 1000000).toFixed(2)} MH/s`; + return `${hashRate.toFixed(2)} H/s`; + }; + + const formatDifficulty = (difficulty: number): string => { + if (difficulty >= 1000000000000) return `${(difficulty / 1000000000000).toFixed(2)} T`; + if (difficulty >= 1000000000) return `${(difficulty / 1000000000).toFixed(2)} G`; + if (difficulty >= 1000000) return `${(difficulty / 1000000).toFixed(2)} M`; + return `${difficulty.toFixed(2)}`; + }; + + // Function to safely access data + const safeValue = (value: any, defaultVal: string = '0') => { + return value !== undefined && value !== null ? value : defaultVal; + }; + + return ( + + + + + + Bitcoin Network Stats + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : chainData && chainData.data ? ( +
+
+
Hash Rate
+
+ {formatHashRate(chainData.data.hashRate || 0)} +
+
+
+
Difficulty
+
+ {formatDifficulty(chainData.data.difficulty || 0)} +
+
+
+
Block Height
+
+ {(chainData.data.latestHeight || 0).toLocaleString()} +
+
+
+
Mempool Size
+
+ {(chainData.data.mempool || 0).toLocaleString()} tx +
+
+
+
Avg Block Time
+
+ {typeof chainData.data.minutesBetweenBlocks === 'number' ? + chainData.data.minutesBetweenBlocks.toFixed(2) : '0.00'} min +
+
+
+
Tx Rate
+
+ {typeof chainData.data.transactionRate === 'number' ? + chainData.data.transactionRate.toFixed(2) : '0.00'}/sec +
+
+
+ ) : ( +
+ Unable to fetch blockchain data +
+ )} +
+
+ ); +} diff --git a/components/market-overview/CryptoCard.tsx b/components/market-overview/CryptoCard.tsx new file mode 100644 index 0000000..234e4c2 --- /dev/null +++ b/components/market-overview/CryptoCard.tsx @@ -0,0 +1,78 @@ + +import React from 'react'; +import { Card, CardContent } from '@/components/ui/card'; +import { ArrowUpRight, ArrowDownRight } from 'lucide-react'; +import Image from 'next/image'; + +interface CryptoCardProps { + name: string; + symbol: string; + price: number; + change24h: number; + icon: string; + fallbackIcon: string | null; +} + +export default function CryptoCard({ + name, + symbol, + price, + change24h, + icon, + fallbackIcon +}: CryptoCardProps) { + const isPositive = change24h >= 0; + + // Format price based on value + const formatPrice = (price: number): string => { + if (price >= 1000) { + return `$${price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; + } else if (price >= 1) { + return `$${price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + } else { + return `$${price.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 6 })}`; + } + }; + + return ( + + +
+
+ {/* Try to load the specific icon, fall back to placeholder */} +
+ {symbol} { + // If specific icon fails, try fallback or use placeholder + if (fallbackIcon) { + (e.target as HTMLImageElement).src = fallbackIcon; + } else { + (e.target as HTMLImageElement).src = '/icons/token-placeholder.png'; + } + }} + /> +
+
+ {symbol} +
+ +
+ {formatPrice(price)} +
+ +
+ {isPositive ? ( + + ) : ( + + )} + {isPositive ? '+' : ''}{change24h.toFixed(2)}% +
+
+
+ ); +} diff --git a/components/market-overview/DefiTvlCard.tsx b/components/market-overview/DefiTvlCard.tsx new file mode 100644 index 0000000..6d371c1 --- /dev/null +++ b/components/market-overview/DefiTvlCard.tsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, LineChart, ExternalLink } from 'lucide-react'; +import axios from 'axios'; +import { + LineChart as RechartsLineChart, + Line, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer +} from 'recharts'; +import Link from 'next/link'; + +interface TvlDataPoint { + date: string; + tvl: number; +} + +interface TvlData { + data: TvlDataPoint[]; + totalTvl: number; + timestamp: number; + simulated?: boolean; +} + +export default function DefiTvlCard() { + const [tvlData, setTvlData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchTvlData = async () => { + try { + const response = await axios.get('/api/analytics/defi-tvl'); + setTvlData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch TVL data:", error); + } finally { + setIsLoading(false); + } + }; + + fetchTvlData(); + + // Refresh every 12 hours + const interval = setInterval(fetchTvlData, 12 * 60 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const formatTvl = (value: number): string => { + if (value >= 1000000000000) return `$${(value / 1000000000000).toFixed(2)}T`; + if (value >= 1000000000) return `$${(value / 1000000000).toFixed(2)}B`; + if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(2)}K`; + return `$${value.toFixed(2)}`; + }; + + return ( + + + + + + DeFi Total Value Locked + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : tvlData && tvlData.data ? ( + <> +
+ + + + + [formatTvl(value), "TVL"]} + labelFormatter={(label) => `Date: ${label}`} + contentStyle={{ + backgroundColor: '#1F2937', + borderColor: '#374151', + borderRadius: '4px', + color: 'white' + }} + /> + + + +
+
+
+ {formatTvl(tvlData.totalTvl)} +
+

+ Total Value Locked across all DeFi protocols +

+ + View on DefiLlama + + +
+ + ) : ( +
+ No DeFi TVL data available +
+ )} +
+
+ ); +} diff --git a/components/market-overview/DominanceCard.tsx b/components/market-overview/DominanceCard.tsx new file mode 100644 index 0000000..79c9f0a --- /dev/null +++ b/components/market-overview/DominanceCard.tsx @@ -0,0 +1,71 @@ + +import React from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info } from 'lucide-react'; + +interface DominanceCardProps { + btcDominance: number; + ethDominance: number; + othersDominance: number; +} + +export default function DominanceCard({ + btcDominance, + ethDominance, + othersDominance +}: DominanceCardProps) { + return ( + + + + Bitcoin Dominance + + + + +
+
+
+
+
+ Bitcoin +
+ {btcDominance.toFixed(1)}% +
+
+
+
+ Ethereum +
+ {ethDominance.toFixed(1)}% +
+
+
+
+ Others +
+ {othersDominance.toFixed(1)}% +
+
+ +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/components/market-overview/ExchangeVolumeCard.tsx b/components/market-overview/ExchangeVolumeCard.tsx new file mode 100644 index 0000000..d55a30b --- /dev/null +++ b/components/market-overview/ExchangeVolumeCard.tsx @@ -0,0 +1,156 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle } from 'lucide-react'; +import axios from 'axios'; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip as RechartsTooltip, + ResponsiveContainer, + Cell +} from 'recharts'; + +interface ExchangeVolume { + name: string; + volume: number; + color: string; +} + +interface ExchangeVolumeData { + data: ExchangeVolume[]; + totalVolume: number; + timestamp: number; + simulated?: boolean; +} + +export default function ExchangeVolumeCard() { + const [volumeData, setVolumeData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchExchangeVolumes = async () => { + try { + const response = await axios.get('/api/analytics/exchange-volumes'); + setVolumeData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch exchange volume data:", error); + + // Fallback data + const fallbackData: ExchangeVolumeData = { + data: [ + { name: 'Binance', volume: 25000000000, color: '#F0B90B' }, + { name: 'Coinbase', volume: 12000000000, color: '#1652F0' }, + { name: 'OKX', volume: 8000000000, color: '#1A1B1F' }, + { name: 'Huobi', volume: 5000000000, color: '#1F94E0' }, + { name: 'KuCoin', volume: 3000000000, color: '#26A17B' }, + ], + totalVolume: 53000000000, + timestamp: Date.now(), + simulated: true + }; + + setVolumeData(fallbackData); + setIsSimulated(true); + } finally { + setIsLoading(false); + } + }; + + fetchExchangeVolumes(); + + // Refresh every 1 hour + const interval = setInterval(fetchExchangeVolumes, 60 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const formatVolume = (volume: number): string => { + if (volume >= 1000000000) { + return `$${(volume / 1000000000).toFixed(2)}B`; + } else if (volume >= 1000000) { + return `$${(volume / 1000000).toFixed(2)}M`; + } else { + return `$${volume.toLocaleString()}`; + } + }; + + return ( + + + + + Exchange Volume (24h) + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : volumeData ? ( + <> +
+ + + + + [formatVolume(value), "Volume"]} + contentStyle={{ + backgroundColor: '#1F2937', + borderColor: '#374151', + borderRadius: '4px', + color: 'white' + }} + /> + + {volumeData.data.map((entry, index) => ( + + ))} + + + +
+
+
+ {formatVolume(volumeData.totalVolume)} +
+

Total 24h Trading Volume

+
+ + ) : ( +
+ Unable to fetch exchange volume data +
+ )} +
+
+ ); +} diff --git a/components/market-overview/FearGreedIndex.tsx b/components/market-overview/FearGreedIndex.tsx new file mode 100644 index 0000000..c4dc209 --- /dev/null +++ b/components/market-overview/FearGreedIndex.tsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, ExternalLink } from 'lucide-react'; +import Link from 'next/link'; +import axios from 'axios'; + +interface FearGreedData { + value: number; + valueText: string; + timestamp: number; + simulated?: boolean; +} + +export default function FearGreedIndex() { + const [fgIndex, setFgIndex] = useState(50); + const [fgText, setFgText] = useState("Neutral"); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchFearGreedIndex = async () => { + try { + // Try to fetch from real API + const response = await fetch('/api/analytics/fear-greed-index'); + + if (response.ok) { + const data = await response.json(); + setFgIndex(data.value); + setFgText(data.valueText); + setIsSimulated(data.simulated || false); + } else { + throw new Error('API request failed'); + } + } catch (error) { + console.error("Failed to fetch Fear & Greed Index:", error); + + // Generate fallback data + const fallbackValue = Math.floor(Math.random() * 20) + 15; // Random value between 15-35 + setFgIndex(fallbackValue); + setIsSimulated(true); + + // Set text based on value + if (fallbackValue <= 20) setFgText("Extreme Fear"); + else if (fallbackValue <= 40) setFgText("Fear"); + else if (fallbackValue <= 60) setFgText("Neutral"); + else if (fallbackValue <= 80) setFgText("Greed"); + else setFgText("Extreme Greed"); + } finally { + setIsLoading(false); + } + }; + + fetchFearGreedIndex(); + }, []); + + // Determine color based on the fear & greed index + const getSentiment = (value: number): { text: string; color: string } => { + if (value <= 20) return { text: "Extreme Fear", color: 'from-red-600 to-red-500' }; + if (value <= 40) return { text: "Fear", color: 'from-orange-600 to-orange-500' }; + if (value <= 60) return { text: "Neutral", color: 'from-yellow-500 to-yellow-400' }; + if (value <= 80) return { text: "Greed", color: 'from-green-500 to-green-400' }; + return { text: "Extreme Greed", color: 'from-green-700 to-green-600' }; + }; + + const sentiment = getSentiment(fgIndex); + const rotation = (fgIndex / 100) * 180 - 90; // Convert to -90 to 90 deg range + + return ( + + + + + Fear and Greed Index + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : ( + <> +
+ {/* Gauge background */} +
+ + {/* Gauge foreground */} +
+
+ {/* Needle */} +
+ + {/* Needle base */} +
+
+
+
+ +
+
{fgIndex}
+
+ {fgText} +
+
+ + )} +
+
+ ); +} diff --git a/components/market-overview/GasPriceCard.tsx b/components/market-overview/GasPriceCard.tsx new file mode 100644 index 0000000..967e0e5 --- /dev/null +++ b/components/market-overview/GasPriceCard.tsx @@ -0,0 +1,107 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, Fuel } from 'lucide-react'; +import axios from 'axios'; + +interface GasPriceData { + slow: number; + average: number; + fast: number; + baseFee: number; + timestamp: number; + simulated?: boolean; +} + +export default function GasPriceCard() { + const [gasData, setGasData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchGasData = async () => { + try { + const response = await axios.get('/api/analytics/gas-prices'); + setGasData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch gas prices:", error); + } finally { + setIsLoading(false); + } + }; + + fetchGasData(); + + // Refresh every 30 seconds + const interval = setInterval(fetchGasData, 30000); + return () => clearInterval(interval); + }, []); + + const getGasColor = (price: number): string => { + if (price < 20) return 'text-green-500'; + if (price < 50) return 'text-yellow-500'; + if (price < 100) return 'text-orange-500'; + return 'text-red-500'; + }; + + return ( + + + + + + Ethereum Gas Prices + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : gasData ? ( +
+
+
+
Slow
+
+ {gasData.slow} Gwei +
+
+
+
Average
+
+ {gasData.average} Gwei +
+
+
+
Fast
+
+ {gasData.fast} Gwei +
+
+
+ +
+
+ Base Fee: + {gasData.baseFee} Gwei +
+
+
+ ) : ( +
+ Unable to fetch gas prices +
+ )} +
+
+ ); +} diff --git a/components/market-overview/MarketCapChart.tsx b/components/market-overview/MarketCapChart.tsx new file mode 100644 index 0000000..128b7be --- /dev/null +++ b/components/market-overview/MarketCapChart.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'; + +interface MarketCapChartProps { + data: any; + timeframe: string; +} + +export default function MarketCapChart({ data, timeframe }: MarketCapChartProps) { + // Return empty chart if no data + if (!data || !data.prices || data.prices.length === 0) { + return ( +
+

No chart data available

+
+ ); + } + + // Process chart data based on timeframe + const processChartData = () => { + const dataPoints: { date: string; price: number; volume: number }[] = []; + let interval = 1; // Default interval + + // Adjust interval based on timeframe and data length to avoid overcrowding + if (timeframe === '1y' && data.prices.length > 30) { + interval = Math.floor(data.prices.length / 30); + } else if (timeframe === '30d' && data.prices.length > 30) { + interval = Math.floor(data.prices.length / 30); + } + + // Format date based on timeframe + const formatDate = (timestamp: number) => { + const date = new Date(timestamp); + if (timeframe === '24h') { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } else if (timeframe === '7d') { + return date.toLocaleDateString([], { weekday: 'short' }); + } else { + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + } + }; + + // Select points based on interval + for (let i = 0; i < data.prices.length; i += interval) { + const [timestamp, price] = data.prices[i]; + const volume = data.total_volumes[i] ? data.total_volumes[i][1] : 0; + + dataPoints.push({ + date: formatDate(timestamp), + price, + volume, + }); + } + + return dataPoints; + }; + + const chartData = processChartData(); + const isPositive = chartData[0].price <= chartData[chartData.length - 1].price; + const chartColor = isPositive ? '#10B981' : '#EF4444'; + + // Format Y-axis values for Bitcoin prices + const formatYAxis = (value: number) => { + if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`; + return `$${value.toFixed(0)}`; + }; + + return ( +
+ + + + + + + + + + + + { + if (active && payload && payload.length) { + return ( +
+

{payload[0].payload.date}

+

Price: ${payload[0].value?.toLocaleString()}

+
+ ); + } + return null; + }} + /> + +
+
+
+ ); +} diff --git a/components/market-overview/MarketIndexCard.tsx b/components/market-overview/MarketIndexCard.tsx new file mode 100644 index 0000000..98f3ba5 --- /dev/null +++ b/components/market-overview/MarketIndexCard.tsx @@ -0,0 +1,107 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, ArrowUpRight, ArrowDownRight } from 'lucide-react'; + +interface MarketIndex { + name: string; + symbol: string; + price: number; + change: number; + changePercent: number; +} + +export default function MarketIndexCard() { + const [indices, setIndices] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchMarketIndices = async () => { + try { + // In a real app, this would be an API call + // Using simulated data for demonstration + const simulatedIndices: MarketIndex[] = [ + { + name: 'S&P 500', + symbol: 'SPX', + price: 4500 + (Math.random() * 200 - 100), + change: Math.random() * 40 - 20, + changePercent: (Math.random() * 2 - 1) + }, + { + name: 'Nasdaq', + symbol: 'NDX', + price: 15000 + (Math.random() * 500 - 250), + change: Math.random() * 100 - 50, + changePercent: (Math.random() * 2 - 1) + }, + { + name: 'Dow Jones', + symbol: 'DJI', + price: 35000 + (Math.random() * 1000 - 500), + change: Math.random() * 200 - 100, + changePercent: (Math.random() * 2 - 1) + }, + { + name: 'Gold', + symbol: 'XAU', + price: 2000 + (Math.random() * 100 - 50), + change: Math.random() * 30 - 15, + changePercent: (Math.random() * 2 - 1) + } + ]; + + setIndices(simulatedIndices); + } catch (error) { + console.error("Failed to fetch market indices:", error); + } finally { + setIsLoading(false); + } + }; + + fetchMarketIndices(); + + const interval = setInterval(fetchMarketIndices, 60 * 1000); + return () => clearInterval(interval); + }, []); + + return ( + + + + Traditional Markets + + + + + {isLoading ? ( +
+
+
+ ) : ( +
+ {indices.map((index) => ( +
+
+
{index.name}
+
{index.symbol}
+
+
+
{index.price.toLocaleString(undefined, { maximumFractionDigits: 2 })}
+
= 0 ? 'text-green-500' : 'text-red-500'}`}> + {index.changePercent >= 0 ? ( + + ) : ( + + )} + {index.changePercent >= 0 ? '+' : ''} + {index.changePercent.toFixed(2)}% +
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/components/market-overview/MarketSentimentCard.tsx b/components/market-overview/MarketSentimentCard.tsx new file mode 100644 index 0000000..5ff0e39 --- /dev/null +++ b/components/market-overview/MarketSentimentCard.tsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect, JSX } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, TrendingUp, TrendingDown, Minus } from 'lucide-react'; +import axios from 'axios'; + +interface SentimentData { + score: number; + socialMediaScore: number; + newsScore: number; + redditMentions: number; + twitterMentions: number; + timestamp: number; + simulated?: boolean; +} + +export default function MarketSentimentCard() { + const [sentimentData, setSentimentData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchSentimentData = async () => { + try { + const response = await axios.get('/api/analytics/market-sentiment'); + setSentimentData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch sentiment data:", error); + } finally { + setIsLoading(false); + } + }; + + fetchSentimentData(); + + // Refresh every 30 minutes + const interval = setInterval(fetchSentimentData, 30 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const getSentimentText = (score: number): { text: string; color: string; icon: JSX.Element } => { + if (score >= 70) return { + text: 'Extremely Bullish', + color: 'text-green-500', + icon: + }; + if (score >= 60) return { + text: 'Bullish', + color: 'text-green-400', + icon: + }; + if (score >= 45) return { + text: 'Slightly Bullish', + color: 'text-green-300', + icon: + }; + if (score > 55) return { + text: 'Neutral', + color: 'text-gray-400', + icon: + }; + if (score > 40) return { + text: 'Slightly Bearish', + color: 'text-red-300', + icon: + }; + if (score > 30) return { + text: 'Bearish', + color: 'text-red-400', + icon: + }; + return { + text: 'Extremely Bearish', + color: 'text-red-500', + icon: + }; + }; + + return ( + + + + + Market Sentiment Analysis + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : sentimentData ? ( +
+
+
{sentimentData.score.toFixed(0)}/100
+ + {(() => { + const sentiment = getSentimentText(sentimentData.score); + return ( +
+ {sentiment.icon} + {sentiment.text} +
+ ); + })()} +
+ +
+
+
= 55 ? 'bg-green-500' : + sentimentData.score >= 45 ? 'bg-yellow-500' : 'bg-red-500' + }`} + style={{ width: `${sentimentData.score}%` }} + >
+
+
+ +
+
+
Social Media
+
= 55 ? 'text-green-500' : + sentimentData.socialMediaScore >= 45 ? 'text-yellow-500' : 'text-red-500' + }`}> + {sentimentData.socialMediaScore.toFixed(0)}/100 +
+
+
+
News
+
= 55 ? 'text-green-500' : + sentimentData.newsScore >= 45 ? 'text-yellow-500' : 'text-red-500' + }`}> + {sentimentData.newsScore.toFixed(0)}/100 +
+
+
+ +
+
Reddit: {sentimentData.redditMentions.toLocaleString()} mentions
+
Twitter: {sentimentData.twitterMentions.toLocaleString()} mentions
+
+
+ ) : ( +
+ Unable to fetch sentiment data +
+ )} +
+
+ ); +} diff --git a/components/market-overview/NftStatsCard.tsx b/components/market-overview/NftStatsCard.tsx new file mode 100644 index 0000000..cf578c3 --- /dev/null +++ b/components/market-overview/NftStatsCard.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, Image as ImageIcon, ExternalLink } from 'lucide-react'; +import axios from 'axios'; +import Link from 'next/link'; + +interface NftCollection { + name: string; + symbol: string; + floorPrice: number; + volume24h: number; + totalVolume: number; + owners: number; +} + +interface NftStatsData { + collections: NftCollection[]; + timestamp: number; + simulated?: boolean; +} + +export default function NftStatsCard() { + const [nftData, setNftData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchNftData = async () => { + try { + const response = await axios.get('/api/analytics/nft-stats'); + setNftData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch NFT stats:", error); + } finally { + setIsLoading(false); + } + }; + + fetchNftData(); + + // Refresh every 6 hours + const interval = setInterval(fetchNftData, 6 * 60 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + return ( + + + + + + Top NFT Collections + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : nftData && nftData.collections && nftData.collections.length > 0 ? ( +
+
+ + + + + + + + + + {nftData.collections.map((collection, index) => ( + + + + + + ))} + +
CollectionFloor Price24h Vol
+
{collection.name}
+
{collection.symbol}
+
+
{collection.floorPrice.toFixed(2)} ETH
+
+
{collection.volume24h.toFixed(1)} ETH
+
+
+ +
+ + View NFT Explorer + + +
+
+ ) : ( +
+ No NFT stats available +
+ )} +
+
+ ); +} diff --git a/components/market-overview/StakingYieldsCard.tsx b/components/market-overview/StakingYieldsCard.tsx new file mode 100644 index 0000000..e69de29 diff --git a/components/market-overview/TrendingCoinsCard.tsx b/components/market-overview/TrendingCoinsCard.tsx new file mode 100644 index 0000000..5460e90 --- /dev/null +++ b/components/market-overview/TrendingCoinsCard.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Info, AlertTriangle, Flame, ExternalLink } from 'lucide-react'; +import axios from 'axios'; +import Image from 'next/image'; +import Link from 'next/link'; + +interface TrendingCoin { + id: string; + name: string; + symbol: string; + thumb: string; + price_btc: number; + market_cap_rank: number; + score: number; +} + +interface TrendingData { + coins: TrendingCoin[]; + timestamp: number; + simulated?: boolean; +} + +export default function TrendingCoinsCard() { + const [trendingData, setTrendingData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSimulated, setIsSimulated] = useState(false); + + useEffect(() => { + const fetchTrendingData = async () => { + try { + const response = await axios.get('/api/analytics/trending-coins'); + setTrendingData(response.data); + setIsSimulated(response.data.simulated || false); + } catch (error) { + console.error("Failed to fetch trending coins:", error); + } finally { + setIsLoading(false); + } + }; + + fetchTrendingData(); + + // Refresh every hour + const interval = setInterval(fetchTrendingData, 60 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const formatBtcPrice = (price: number): string => { + if (price < 0.00001) return price.toExponential(2); + return price.toFixed(8); + }; + + return ( + + + + + + Trending Coins + + + {isSimulated && ( + + + Estimated + + )} + + + + {isLoading ? ( +
+
+
+ ) : trendingData && trendingData.coins.length > 0 ? ( +
+ {trendingData.coins.map((coin, index) => ( +
+
+ {index + 1}. +
+ {coin.name} { + // If image fails, show first letter of coin name + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + target.parentElement!.innerHTML = coin.symbol.charAt(0).toUpperCase(); + target.parentElement!.style.display = 'flex'; + target.parentElement!.style.justifyContent = 'center'; + target.parentElement!.style.alignItems = 'center'; + }} + /> +
+
+
{coin.name}
+
{coin.symbol}
+
+
+
+
Price in BTC
+
₿ {formatBtcPrice(coin.price_btc)}
+
+
+ ))} + +
+ + View More on CoinGecko + + +
+
+ ) : ( +
+ No trending coins data available +
+ )} +
+
+ ); +} diff --git a/components/market-overview/WhaleAlertsCard.tsx b/components/market-overview/WhaleAlertsCard.tsx new file mode 100644 index 0000000..72cb375 --- /dev/null +++ b/components/market-overview/WhaleAlertsCard.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Badge } from '@/components/ui/badge'; +import { ArrowRight, Wallet } from 'lucide-react'; + +interface WhaleTransaction { + id: string; + symbol: string; + amount: number; + value: number; + from: string; + to: string; + type: 'withdrawal' | 'deposit' | 'transfer'; + timestamp: number; +} + +interface WhaleData { + transactions: WhaleTransaction[]; + totalValue: number; + timestamp: number; + simulated: boolean; +} + +export default function WhaleAlertsCard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/api/analytics/whale-alerts?limit=5'); + if (!response.ok) throw new Error('Failed to fetch whale alerts'); + const whaleData = await response.json(); + setData(whaleData); + } catch (error) { + console.error('Error fetching whale alerts:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + // Refresh every 2 minutes + const interval = setInterval(fetchData, 2 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const formatAddress = (address: string) => { + if (address.includes('.') || address.includes(' ')) return address; // Exchange name + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + + const formatValue = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + notation: 'compact', + maximumFractionDigits: 1 + }).format(value); + }; + + const formatAmount = (amount: number, symbol: string) => { + const precision = symbol === 'BTC' ? 2 : 1; + return `${amount.toFixed(precision)} ${symbol}`; + }; + + const getTransactionColor = (type: string) => { + switch (type) { + case 'withdrawal': return 'text-red-400'; + case 'deposit': return 'text-green-400'; + default: return 'text-blue-400'; + } + }; + + if (loading) { + return ( + + + + + Whale Alerts + + + + {[...Array(5)].map((_, i) => ( + + ))} + + + ); + } + + return ( + + +
+ + + Whale Alerts + + {data?.simulated && ( + + Simulated Data + + )} +
+
+ + {data?.transactions.map((tx) => ( +
+
+
+ {formatAddress(tx.from)} + + {formatAddress(tx.to)} +
+
+ + {formatAmount(tx.amount, tx.symbol)} + + + ({formatValue(tx.value)}) + +
+
+ + {tx.type} + +
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/lib/api/alchemyNFTApi.ts b/lib/api/alchemyNFTApi.ts new file mode 100644 index 0000000..fb63ff7 --- /dev/null +++ b/lib/api/alchemyNFTApi.ts @@ -0,0 +1,277 @@ + +import { toast } from "sonner"; + +const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || 'demo'; + +const CHAIN_ID_TO_NETWORK: Record = { + '0x1': 'eth-mainnet', + '0x5': 'eth-goerli', + '0xaa36a7': 'eth-sepolia', + '0x89': 'polygon-mainnet', + '0x13881': 'polygon-mumbai', + '0xa': 'optimism-mainnet', + '0xa4b1': 'arbitrum-mainnet', + '0x38': 'bsc-mainnet', +}; + +interface AlchemyNFTResponse { + ownedNfts: any[]; + totalCount: number; + pageKey?: string; +} + +interface CollectionMetadata { + name: string; + symbol: string; + totalSupply: string; + description: string; + imageUrl: string; +} + +export async function fetchUserNFTs(address: string, chainId: string, pageKey?: string): Promise { + if (!address) { + throw new Error("Address is required to fetch NFTs"); + } + + const network = CHAIN_ID_TO_NETWORK[chainId as keyof typeof CHAIN_ID_TO_NETWORK] || 'eth-mainnet'; + + try { + const apiUrl = `https://${network}.g.alchemy.com/nft/v2/${ALCHEMY_API_KEY}/getNFTs`; + const url = new URL(apiUrl); + url.searchParams.append('owner', address); + url.searchParams.append('withMetadata', 'true'); + url.searchParams.append('excludeFilters[]', 'SPAM'); + url.searchParams.append('pageSize', '100'); + + if (pageKey) { + url.searchParams.append('pageKey', pageKey); + } + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`Error fetching NFTs for ${address}:`, error); + toast.error("Failed to load NFTs"); + return { ownedNfts: [], totalCount: 0 }; + } +} + +export async function fetchCollectionInfo(contractAddress: string, chainId: string): Promise { + if (!contractAddress) { + throw new Error("Contract address is required"); + } + + const network = CHAIN_ID_TO_NETWORK[chainId as keyof typeof CHAIN_ID_TO_NETWORK] || 'eth-mainnet'; + + try { + const apiUrl = `https://${network}.g.alchemy.com/nft/v2/${ALCHEMY_API_KEY}/getContractMetadata`; + const url = new URL(apiUrl); + url.searchParams.append('contractAddress', contractAddress); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + + return { + name: data.contractMetadata.name || 'Unknown Collection', + symbol: data.contractMetadata.symbol || '', + totalSupply: data.contractMetadata.totalSupply || '0', + description: data.contractMetadata.openSea?.description || '', + imageUrl: data.contractMetadata.openSea?.imageUrl || '', + }; + } catch (error) { + console.error(`Error fetching collection info for ${contractAddress}:`, error); + toast.error("Failed to load collection info"); + return { + name: 'Unknown Collection', + symbol: '', + totalSupply: '0', + description: '', + imageUrl: '', + }; + } +} + +interface NFTItem { + id: { + tokenId: string; + }; + title?: string; + description?: string; + media?: Array<{gateway?: string}>; + metadata?: { + attributes?: Array<{trait_type: string, value: string}> + }; +} + +interface CollectionNFT { + id: string; + tokenId: string; + name: string; + description: string; + imageUrl: string; + attributes: Array<{ + trait_type: string; + value: string; + }>; +} + +interface CollectionNFTsResponse { + nfts: CollectionNFT[]; + totalCount: number; + pageKey?: string; +} + +export async function fetchCollectionNFTs( + contractAddress: string, + chainId: string, + page: number = 1, + pageSize: number = 20, + sortBy: string = 'tokenId', + sortDirection: 'asc' | 'desc' = 'asc', + searchQuery: string = '', + attributes: Record = {} +): Promise { + if (!contractAddress) { + throw new Error("Contract address is required"); + } + + const network = CHAIN_ID_TO_NETWORK[chainId as keyof typeof CHAIN_ID_TO_NETWORK] || 'eth-mainnet'; + + try { + const apiUrl = `https://${network}.g.alchemy.com/nft/v2/${ALCHEMY_API_KEY}/getNFTsForCollection`; + const url = new URL(apiUrl); + url.searchParams.append('contractAddress', contractAddress); + url.searchParams.append('withMetadata', 'true'); + url.searchParams.append('startToken', ((page - 1) * pageSize).toString()); + url.searchParams.append('limit', pageSize.toString()); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + + // Process NFTs + let nfts = data.nfts.map((nft: NFTItem) => ({ + id: `${contractAddress}-${nft.id.tokenId || ''}`, + tokenId: nft.id.tokenId || '', + name: nft.title || `NFT #${parseInt(nft.id.tokenId || '0', 16).toString()}`, + description: nft.description || '', + imageUrl: nft.media?.[0]?.gateway || '', + attributes: nft.metadata?.attributes || [], + })); + + // Apply filters + if (searchQuery) { + const query = searchQuery.toLowerCase(); + nfts = nfts.filter((nft: CollectionNFT) => + nft.name.toLowerCase().includes(query) || + nft.tokenId.toLowerCase().includes(query) + ); + } + + // Apply attribute filters + if (Object.keys(attributes).length > 0) { + nfts = nfts.filter((nft: CollectionNFT) => { + for (const [traitType, values] of Object.entries(attributes)) { + const nftAttribute = nft.attributes.find((attr: {trait_type: string, value: string}) => attr.trait_type === traitType); + if (!nftAttribute || !values.includes(nftAttribute.value)) { + return false; + } + } + return true; + }); + } + + // Apply sorting + nfts.sort((a: CollectionNFT, b: CollectionNFT) => { + if (sortBy === 'tokenId') { + const idA = parseInt(a.tokenId, 16) || 0; + const idB = parseInt(b.tokenId, 16) || 0; + return sortDirection === 'asc' ? idA - idB : idB - idA; + } else if (sortBy === 'name') { + return sortDirection === 'asc' + ? a.name.localeCompare(b.name) + : b.name.localeCompare(a.name); + } + return 0; + }); + + return { + nfts: nfts, + totalCount: data.totalCount || nfts.length, + pageKey: data.pageKey + }; + } catch (error) { + console.error(`Error fetching NFTs for collection ${contractAddress}:`, error); + toast.error("Failed to load collection NFTs"); + return { nfts: [], totalCount: 0 }; + } +} + +export async function fetchPopularCollections(chainId: string = '0x1'): Promise { + try { + // In a production app, you would fetch this from a backend API + // For now, we'll use a mock response + return [ + { + id: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', + name: 'Bored Ape Yacht Club', + imageUrl: 'https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format', + floorPrice: 30.5, + totalSupply: 10000, + }, + { + id: '0x60e4d786628fea6478f785a6d7e704777c86a7c6', + name: 'Mutant Ape Yacht Club', + imageUrl: 'https://i.seadn.io/gae/lHexKRMpw-aoSyB1WdFBff5yfANLReFxHzt1DOj_sg7mS14yARpuvYcUtsyyx-Nkpk6WTcUPFoG53VnLJezYi8hAs0OxNZwlw6Y-dmI?w=500&auto=format', + floorPrice: 12.2, + totalSupply: 20000, + }, + { + id: '0xed5af388653567af2f388e6224dc7c4b3241c544', + name: 'Azuki', + imageUrl: 'https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format', + floorPrice: 8.9, + totalSupply: 10000, + }, + { + id: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb', + name: 'CryptoPunks', + imageUrl: 'https://i.seadn.io/gae/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE?w=500&auto=format', + floorPrice: 50.2, + totalSupply: 10000, + }, + { + id: '0x8a90cab2b38dba80c64b7734e58ee1db38b8992e', + name: 'Doodles', + imageUrl: 'https://i.seadn.io/gae/7B0qai02OdHA8P_EOVK672qUliyjQdQDGNrACxs7WnTgZAkJa_wWURnIFKeOh5VTf8cfTqW3wQpozGedaC9mteKphEOtztls02RlWQ?w=500&auto=format', + floorPrice: 3.8, + totalSupply: 10000, + }, + { + id: '0x1a92f7381b9f03921564a437210bb9396471050c', + name: 'Cool Cats', + imageUrl: 'https://i.seadn.io/gae/LIov33kogXOK4XZd2ESj29sqm_Hww5JSdO7AFn5wjt8xgnJJ0UpNV9yITqxra3s_LMEW1AnnrgOVB_hDpjJRA1uF4skI5Sdi_9rULi8?w=500&auto=format', + floorPrice: 2.1, + totalSupply: 9999, + }, + ]; + } catch (error) { + console.error('Error fetching popular collections:', error); + toast.error("Failed to load popular collections"); + return []; + } +} diff --git a/package-lock.json b/package-lock.json index d94f7b5..a9eba6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -309,6 +309,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -317,20 +318,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -345,70 +332,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "peer": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", @@ -437,43 +360,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "peer": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -625,20 +511,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", @@ -2795,6 +2667,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3214,6 +3087,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3227,6 +3101,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3236,6 +3111,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3259,6 +3135,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -6388,7 +6265,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6398,7 +6275,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -8676,6 +8553,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8703,6 +8581,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { @@ -8753,6 +8632,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -9205,6 +9085,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9340,6 +9221,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9481,39 +9363,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9663,6 +9512,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9730,6 +9580,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -9754,6 +9605,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9989,6 +9841,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -10006,13 +9859,6 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "license": "ISC" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT", - "peer": true - }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -10108,6 +9954,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -10182,6 +10029,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -10724,6 +10572,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/diffie-hellman": { @@ -10753,6 +10602,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -10840,6 +10690,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/eccrypto": { @@ -10922,13 +10773,6 @@ "node": ">=4.0.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.119", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", - "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", - "license": "ISC", - "peer": true - }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10982,6 +10826,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/encode-utf8": { @@ -11286,16 +11131,6 @@ "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -13016,17 +12851,11 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, - "node_modules/fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", - "license": "CC0-1.0", - "peer": true - }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -13055,6 +12884,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -13189,6 +13019,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -13282,6 +13113,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -13400,16 +13232,6 @@ "node": ">=8" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -13500,6 +13322,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -13520,6 +13343,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -13532,6 +13356,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -13541,6 +13366,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -14139,6 +13965,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -14190,6 +14017,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -14240,6 +14068,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14298,6 +14127,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -14349,6 +14179,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -14537,6 +14368,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -14585,6 +14417,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -14677,6 +14510,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -14939,6 +14773,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -14951,6 +14786,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/lit": { @@ -15194,6 +15030,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -15203,6 +15040,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -15306,6 +15144,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -15408,6 +15247,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -15611,13 +15451,6 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT", - "peer": true - }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -15697,6 +15530,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -16035,6 +15869,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -16121,6 +15956,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16130,12 +15966,14 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -16186,6 +16024,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16284,6 +16123,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -16311,6 +16151,7 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -16339,6 +16180,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -16356,6 +16198,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -16375,6 +16218,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -16410,6 +16254,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -16435,6 +16280,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16448,6 +16294,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/preact": { @@ -16633,6 +16480,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -16956,6 +16804,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -16979,6 +16828,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -17103,6 +16953,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -17143,6 +16994,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -17369,6 +17221,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -17666,6 +17519,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -17678,6 +17532,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17763,6 +17618,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -17964,6 +17820,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -17982,6 +17839,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17996,6 +17854,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18005,12 +17864,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18136,6 +17997,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -18152,6 +18014,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18164,6 +18027,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18240,6 +18104,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -18284,6 +18149,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -18339,6 +18205,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -18385,6 +18252,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -18401,6 +18269,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -18482,6 +18351,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -18491,6 +18361,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -18591,6 +18462,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -18622,6 +18494,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/ts-mixer": { @@ -18771,6 +18644,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18981,37 +18855,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -20465,6 +20308,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -20642,6 +20486,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -20660,6 +20505,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -20677,6 +20523,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -20686,12 +20533,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -20706,6 +20555,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -20718,6 +20568,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -20845,17 +20696,11 @@ "node": ">=0.10.32" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC", - "peer": true - }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/public/Img/market-overview.png b/public/Img/market-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb705719ebfcab5cdff919c03f9335330fde680 GIT binary patch literal 275006 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 z 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%0DV8IM8Va)b#3Xlltq| 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 z zOk^AwSo)QoPzmhHoXMHp=TZGHv!cwO@2-cR-Efd4@RZE1KB(o{!R?te)#IQ)ukGrZ zsJ*)r_;5Z;{_nboT`R`VvAdkdqoT70*LUC(%1L&IUgwsfHKC5L!0whc9^cur{LwO5 zNMa5=rQ6fF!@iBj{{7A~7))`X-0t2??W`r@^8bh7_}zA9&=4~!mY;tlF)8oxHnT;Y zX0hxtE-qN{wGiq<_Div6tF2*MsF@c)%7Q>i}6FvNI}dweo=wQZ8huN33j(!J~kKcnbjUe>X~dcQ&Epw}R3CYOPv{ zop^wbYv8M&jxTo|KGX>P?wW__SnvY!|9Xs3nB6A@4}k^NwAsbnCL23M;^EFlXDE2R zOx|8@?X?|qtfOUsnB!dwi5fY$JH&v9ta1_8{#{W&M+iW3KfpA>6aG{|7nM& zz_mw)#X=qa9kXsrZSv4R8vQeoHNNmziBZheI{>LZwdK?4=sb?f{Yb+Cd(Ch=d`A-n zJVE$aM@YG|B?Ss2-Spi&-wvg#{)OU>u?}R|Rk|jBZ*5nDtVIBy90+i{bw)`^r2Rou zro{hZn};%6N|A@2#aE{qjA~$@=&6)1)I)G6_Ava@|M*Ed5(sT-^YbvdXVFRgz;ri4 zArG|BY4a9Z8x}So20@}7RR;C_6v+PP@)Vj&>RP+t?N30<0aNI|Q)u1)V#;e?^|H=i zC>>n{u>4o_2X$skx3tVdiVJj%fCv_}I9vGDMlPTG{={QB`mfXSh*jVYhjQ@GTn&}Q9R%Y_E#o(4F1H*$zlc#0749$olZnOA4`qcZg%F0&gZ zMCEQp4{D*h75%=;{OTVqGI-K@y&UztNzfRq3|T~lv|+&9k=8om$&yA z6|=P&xxeV1P`%;b3*Q2jUJZUemA}2ZE88aUj7Bj-(jmmq(3J+?`ByUDu56)*qwKK` zgczi6m+T?9#OnV@S<{xa&^bvsEA!?as|qN1)#l5x|B|UCX=|6ECeeb#7B4PEKmRQ%&2j zNj%>=@aee=p3&`(B*{IbyQhbqR6f`^+&R^mm*_F!GaHs5D8%RgoY^O;M#1D%S1lA~ z-=@)jOjXO}GE+y5&B@7GjVC{0UVod{P|8KhyTr)M<2%_?VtxO}`&@f$J=S{q`yP})zcN{P-IxB??3ejeMd;pSI==ScXt#;Z z@14h>zFci>L$t>>9p_6KeNRy}I+6EG!0KlXDK)nQKfzDSZvS`grVADa3AZfY!0;H%OIk=`gpdXN4o z%JI$$mRh}c_hPhF#dtZ9NNg>(?U^rY_ExT0ALerzOO9-pJdjJ%-QB%F?5I{Fd61sZ zckxZN^m`8_EV%VATX-)9h|gy)`P|C!nWQJQ7tel@ii(wgc@C2AGp+2shUf_df@ZHY zdA`l1&wZ4z5S?z@Q#>{jYu_GOJ)ewA(Hv{uTyJ(+n=FAGM?S(_{F215W0{ryT`zZN zWH}CPQvIhY=ak{5tf`GAILBedMlBsx+&iQRx~wlnDRy3Eb6#b&14Hh>u`9l>sxr)Z zP{&PYAK!v>SS@c>#C}hXG|KRtD!WN-?B#$4kkEJ-6?M$iU&eVf&W+e@ofFr-h&;n% z(2s+Td5_XmTf#=+M)T&1w8zF=0v{UQ4HG(5wPZwY>R%7)TWv2PX4!jVr54gv#=eP@ zG~TH~u!H(u12&&wg|S5(3`ytxrn-!PwPkq^^H!C^T=<6Afm;J0QQr z&5HB2ZT|F0ubwX1e2b*T4E6rycXt27{4ixPIg2+A&jJPLn3%LeS@mjss=i5YuH&-X z7UP#HZwmK1hix=qR`}}83xb-k-!!!ilb5QOs)!R4uUU^DJN75ubLzWGhheI9PL-DQ z@&}4Si)JQW>$`!FvKgO_beruicd;J2et~l*lAnJqL)~P6-)KLIr_gwPkQzO5hr34w zvwB-bjaAMALXxxA^OKm`cQWc~FzWGXV~iiP(f=KV<1Q3Y1)KSPV;8|Ex99nTJ*s z*PpY2XJ+?Sd6pElclTDf^*A+4x+x}nEyiY-*N@D(TW1x?g?1)vu8+F*6x*U#HGxTs zyDCW4J?wI3Sw)MmwCI^71Ku8k;K^V{@eIs$>6Nc4xI#YM;`Ap9ed{bn28P0(5_{&h z>Nmy5=|hT`H$IO}!1-}5oa%nq_!O|L+cjuB+s$Vg8PG!E>f%37G z*ok-7pJx`e_hB~4#Z^__iksb8vrIiAQp+I0rU7;Divr(G!YU?it4Q9>DiZQPx5l?Q z%jepbqJ&wZWX%W=zzH#lH zNmfyDPENCt*GwIHRmhIo=)pTl=RYr%LOl^hzeUG+1)v2hP2*rx+`?GC?xUI8%R@)MYLH1sWAgkYS*_v*B?x{YTOBmx2iP;w# zyyp2ICFKy9K)8 z@@&1{OTFDi){(4ZE6tdV_c?!6o~l|b${`5g8Cc{zrav59C@!f|s&FIOO|Z+Y4pP^o zVRNeWlGWvKG*zG$-^@93&(+aDR7U<3mUX~;?-05YKvTq%o6O!Ca-nGj1x|Vj-Wn0h ze@LVbVAYP!Kp9*VC5oRCau(e0!&)tYj-g2$30~@$bv!2X-Me=V0&i025~3)NmnhXJW}s=Pqs^=Q-sUe_^}aN8?i4RHO|8bHCj)5x_o6{5?BhePDa)yS6VlWeUv*Y28R{*ZqZ#|Mj6alK? z1F&WC#)gk*F!b+{aB5AK0`uqs3c107Ve)60Q~iv@878zcs#fu2ijFLN2inkEK16CY z?TGK=%p&>F8+-TeEnjHYUnuXW%AVnMFGp=HcLwdz8v#1-?z{VP8lT=P#>^c3+biuR#!B9}!3DlPI0nu`v{&iJG4f_=~JFQK>_vlQDm&rSw9s!_?nLcz|qB6e4M?c2f z`a}6B=1g!t$nQ`*KxKRj{B=MREs78)-#Yhds-GeP!(uF=Ja{kUvP9tevM$ft?lAw0? zsR{rJF2ESm&!scZ9%7Y4W}jI^$H2gFd&~^5Rr%Uf#brUkBv!eMp?X9V5>|ht;=D1M z?us6BO~Tlkni`K9{$oW>qu&nW!$943{Q70m8>Ml%xlD(LNSUzw6j?p<#`JT7z^fuH z78VxdiFk?4u@^%+?I}4WP;hY1hG}R9&)ku1SV4u)lTTJ(7;U?Uj6GT0LyWf=?)3h( zm-*g=kwr=Z_m;yw_?x;?wZp(ksnD|3gU&j@^1zA_SvB_j+YfYy@}8%qkCN7!T4sS>`x zlW7$X0^X=~yc|~6rW7kQhN$X=KZzk$&d!Bm!N3Gho;*1!#GWRBua8Q)SX?y{W1T_j z&GA{l;wRDcxSFC7vFtuTfkL+3Nnn?Y#rU*WgeB)&u6vL?`+FWNWt1^Bl>HeWZ~GP} z&L$TtY~PL{=WG_`DG*2-QzWZ~hzl@=Tdzx`jk}HXtLWe&?vy!Ck?$5y{O7)js{opN zuprV0Mjt&QYrIt60WxBjvF5JnGh)2_ks24cpSyXSkmFONvB6jkQ^Tzr+4h#ZC?`;+ z1R!Qv2$xCD`&pc9z;QPt%k@H0bmoWl(kh~6PCkX?JXnOJ)dxYdE0yGW?dppo)u9te z+j^p2dzdKL3vax5pPE~Lea*X)k`AfGPIQo4Udd0#hA2`;X;Dc@55nt0$3EdILh_tO zl|2y%*jtkyzsM3IVP+3@QZD4vUJ4155_rweh^W_0fN^1BVq#El)`c8S@~XHS2@;<*@R=35VMK2Iu~eODId=+huZr0{y_nre9U=#(b1V=i&CGwt(hRsCyK0@AUvWHNa%k(qq2 zm)tiK?55En$QLfZIG3>LW#|bxuT=@%AeI~^PO(oT%ftD6%{ck%F zsmpf2fr}e!q>^F@<+C#GSyo=98D3KMIpxF*0laDcWXQ&!kcsK(To}|W?_LcU%dW-6t{RFGgoa+D;$gwD zp{#jsKOOL;=#Vb2>}dwH6KzNJ%<-HOP?0thdK|~DuOXHNT}^7w(w}S+?w1&DUVrzT zDL==9fF({UpObZ2wgK%JoYWl$nh(iE2*z4s0pno$Q08M z^>&s4Ko%KsmxPOL1&tDLXnx}%;`UM~63^%NFXM!93;|^f4c!U0L#3AaSq|}=3dXCU zK|`H@fNJKG`GUrbyRxfSt?2Q2ljGwBi2JZ`Np+}Ht$@K=SgPi$4+UVPpt($BffEQr zPwq=d7OZ2)z0gnkev>(ip0b^(L^ix13#+nk?cXc@q1iEo9<3}SBvd?HabZTsvGI6= zo&Mlnk+F7>FIF}Q--=zvwbp9)QfM$++`r#qgds01hAQypX&X5BR)I0aQlf{HYP~1W z%-9njKk^qMTv7%6GPyX}GkIFuz_ezQOzJouPpaA6C_mhd`>PqifvxoHtB_9a?9z{Y z;QcOwtJ@qk`m@w~(%KlSoIgD0zQsvK4t1wpLC-GMb3^N&8RktjVb%2y6h5kMoAqZ4 zba=YY``bt*lt{02HBTTT^@EUfCTz^75+0e@ele*;tBRIkhQqbH6?n@&BNHbaCi+Le zncf7Yp-w*O)bG6%WSP*Qwc^$@JLmK3(ZQ{!V zq?1pZaC^uV0s-3nH{GUwT9(-MXxXmKr%Kx@B!?Ca1zxZ&!SUB2vQS5DY=d~y+IUw4 z>`WlklA$)dBW&A!hmGKCZH@PhG;s8*nkpZ+0RvVvytEW+SDwB~w(9q!#~&AWoyY~s z6r5gLBIS5)#@)MEcJUuteBK4BBSLbg)&L500($M<+6-`5bLjcz7Z<<%dFq7dEkNlR zOC`f^M?fHL03UAj6VMlXvu0?10z{@{s))6(E}OCt?rA>#h}21K@BN4jhCU}r>%J#{7Lxk{E@1F+{Y2; ztiDj%HQT&_Xk}zocKP0t>^&QKx^~_M+{~5@weJI9HlR(xp+GhX{GD14+Y1O z+dsZha8T~!w8Hy>S9+7l{j<++7UnzvJSE!3-Y&h(m5c9l5nYFnWCOg5+~W1Q%%7y= z_7AT2k`@)EIU1eOjbFiOEA}dZo+H6!rd_shyu;B;cTk}1yU2?Q;Dn4PHGbKP?wf1< z#&P@L(Fs+*9v{u5J77b}=FZmDz*kf0v3dRZi7p8B-2aKWK6JQ$%m#d8EV}|;oYuGt z2~TJqbW)Kf7fJ%e&#*0e_jpEGULhZGqOoC%DiSkKB?0DS)-ww+dC#j^bBG>5$R&CI z*Odj&#@|mdlQZdYnF%3Ti~Z!5>l8t>pRIyxn4PLNuQBT@T~ajVSm^~HwNLZ7#zap^ zGoX|WyQ0YmeY-r@$A0MWPp1&C72BUUb6t6O!!@-2Z{yY0$P8vdCd~Q_yv~>T3|I32 zxQYdyixYXp>;Ej>1}qtckNdg^p`{#*Sa84oOrB{zvG1Il-iZ2R3 zrn14B?zr5zEZ1W*RV446>`8bWRx8=+&3a}p-6yK>9*)>wV7U5qU*j21a!rx0xAvCv`T``rhQ2|koc;d3j`>(&dN5#HJAwj!EK%rod1y;+Nm81kZ zO5?z#C9DbizyGob)z2WGUl9gISG!F@fv>8kjGpa6W|vR)GV|t8O^*St$i8NxmAF(r z6fBPOj^A7k*}OL-Qe15MQ=9%<^q+z&lLGRgpZmbCAr$*=&?<2#3iOZ+*X)_?GyD4b z>@f3*%$gyww#%{64b7GLglCal=sxfSx}P(7eCc-FAA@+640ENIx@dCp^YZeVNbP~> zujXFLVYUUwZz~GA>7^^HuK9F#rp}Oyin~7;;P(ZY`qxhup?~7sOZ;#(ZM4RUZU(j- zB1`~12w|jly)`7L)h*P47VWYK`dW;GWEoFT+PdQ3ygcq=_qAAaF9uPkL`~{=rZ_DR z`EnL3v)oc!Td{jTS^`GPv*AKXC##&zMP&nCH<$52z4MWnl}IFZawm(hx-hE!SJ%a`JIFJWx4!zB3^nUZ-cZ+t_f0FEy1h_GfF&A4gJyG zozWC4tPr}OSFqjLj1}OaND`UNt<~g-ZqWRH$$_*X7;-Ed#k%2 zuM6NUPwQS>9ts&pVwS6`t9JswH*9WB#JQPh%5>uroILB5Se}Ne`l7DxOMcP$VbA$; z|8oDqBg7;1<;9wL-0#y8vh%r0&tV_GIKBVxqvwV8<-Nb*@v24tnn%Ux+E8O-w3FSb zYo}QM%%M4)nj6JQYmKbd%2RRf%`$3Ol0@Z@i1=A5ac9LjJ02z~hdH7w8M!yYPX>crAg znhSb`_XA@kJzc|>j(2r+Me&Mii+|pB7!MG~$c7Z2j!gH<7)-Up(NUJ}Yw?)mmW~o@pQ(h7MW|j^!JJ9gY0RIQbg+DiAiX>hs|b8}BVGkvR)AVnj1?`6-# zTkjC7>xO0tKC5F^QwTQtQRU&|BV|`Md?wSdqWwE>dh?QjBdKaB!{V{qWcTR~BhL#+ zWO`wJ!q{hJKjN+B=NzpxW_x;a_U60tCFQo(R#jEihaW#)YkVVGP`~stzn~!R!HgN| zA%4iAQd+;?^YOE!e@NJ)M~+PYbZVB#x(UOb;Wc=XpMUX6W3czev{F5?H<1oNE7)B|Y* z&RHq(mHyhZAgNRNKmZp=JM`zd&Kz9T-&y9A>XA@@+1#)zINx1ttDjm!N!&vcJ^atp zAN%c6mKk;71_*NomJDR^oM#^IuG&D3!7KntY5%NVe?ij?G&VafU8Hqw?RQ|K);hQ%M zz(^IbjIUq6{)K18I1BGZNo_2nCweQWPuh12+xIb`?l%hH&$6>0NUq;NS---`0&FAdrQlu`K6YB?X~GGC-epyReAg)CB=lw^;&UkjJe!k zyXVDn05zYijLiN$k5Ivqi#j(q8>27H5V1M$^Yc#$2ndvdfoVxqVZO4C860@doV~bt zsfbNLfEJZ&QL=E*#^tBWCc1Qd1OKz>L=weV*t)}j?2&7?l#VfqQPI-UK3w)*08gb% zX&T(jcS%Y6PSyxxS54Ngd~IyJk*XYrnh`I~C=ETzTZ)2hyQQLnA5Fky3#pBaj%u%Y zGNa6!Vv?t#hXw~*09u#G71{Jvmg)7^kQZY;F6m_VK6)7z_5>V)eQ{zgAHk=6CKGVR z6udNkr8ZM)dins6@FDV2KR&CZFQeOYX)1^%SOktCb#z{UCQ+pyzEQLJ^~;yqm;YQR z&b#^I`Cpzo@m9a*#>CHJIkLnXkdg=Fg${yb^6wzwzAJiqbRP%!4;-UGp=d{BgN1MsoqPw+z@fFP59jz0e+0@0L#nFqP1pAG=c@!xZ9M|w^Y)5bp&8L6(9LXN5+}>p^%O zX$ea?&&kPod20RG^G`IeMqCJUh^W(u3BYYN=Gk(;8|TzG28h>XWe@OHFPllM{0@Ux z5Qe~NjMIeIG7{gve_u~gY<+Di<^ms|Hgkq{-5XIS4fBBV)(lNC9l=`Sa&hUEzBg zaaSJMAiDi8JR^VK84s#{{CLl2JpN{H!57DgJbZ>{NmLY#^VCn>6_?wwHQeXer~4*! zmsds;{>uBX@bM_$=TJ6}FoxVF)U+2ICbgR|n zX=!QQO;iWvZQJwTMPITA_At%^M%*#{Q0e17<;`6a9})U!#&fe zicUsGrV|eP`n=|pAYjEP0xyny_3G7MIUU#u?EB2jOdLhr(#`&AZ$yupwXu(Dbc3Op zh7W__9dRg&PYNa_Clk1pv|YZ$H4G0A!y)DKik=S}u8Uf}>n~A*Po{}qfcD@!(}w%q z=`OU?G>>cHJb%6xIOS*4!p2G(Y57T2@j*&=N+UJ3JY!o;+kM|RHyy#Plm(6j{LA#Y zuI{hF!Zc4e7ayN$`6nE@h4<9LEgQzK8O1N)yyYp!|K?b@y82S;D>H>%(%GjIAy=Oq zc&D8MN!OqF`5k|MNmJBMu{0a55C7Bh!DDaz(9B!o8Xrw5+~gH7)M9Tb+S%Tz9`dPS zP8%+L|Nh*KbI<}@}fQ%@N=j9Og=o`{{T+r`E5G+@7%qsxzhP; zwU1JeHE685oEH@5@6l28*HAVDp~%TOND!|}4YCLeQ$uVx8g1ah@TJPt<;TeA$w9K= zizaH+eNcxcaOlj-J??OBmOKPAMW&;+^vXk&R5|xUXQgyF<_mR5nrO@a!jUKMr7zk3 z{_;wv+?f;H0WRl1EmwQbU-f!fiHPU?_nVqi5(ELIf^Wh@OMRYvh)Fj9o&M#^7lfm| zlarGXj)AC(>hSaS8jdy3##@?qEG)1Nw%vv4LM&eZR^0z|T=>S_b7jEX){R7z!)<+C z-7&lDOP3{Y=Lr3{>t)zpoY(TwzJ86o!6ki5Z1jy2gS{Rkxah~RX7R-e`7_Q&6B4+x zE-;GeNhPVw;?LCvQ9XUB$8S=L!l(3hx8|Yo9yKd6R_q)c2hq5|4-gy7Qf4i-?l^&D zFZ2|)|7rezjC}_<6@35q5uq}p>|GKen`F<7vS*T+y~@ZIAzMXJGLubqcA2T{5hc51 z6WP4qpZY(~bN`?By{`AZ?yIXf&T)R@J3ixko{D$XbEuGgjy1ejdo(`2Ih-QhakQ>- zzWBp3WFv_wHT7-%@g}ou}&8dX!^dyeKFDxGJ>ZSG zne_L&V%qvled~=abh4sOa~f1W`~3k`NyWVU_QT|_p1Xv7nQ~9%l3-Y;^D7$seUq6l zY|~r(%-OSVs_CE}9T>@A(`g1E)4rSw(hB}uYatUKlbtC zAXrlt4vr5dh7|D!yDXR#-DF?ruQCT;#~;>p_?u#(C+?cuqdAYy)K5kf(p~T82d^W` zOzOSA^+TPi;;Y;X^XTa4R_LcjxQIg^K0LYJa?{k*wCtJQ2<=Se=g)esX$NX9P#)a` z_Kl63yA_(uABzR?TfctfM(93x@IZZomqRB{Qd(NN&qv91Iw!Zl29As6$EN|nkZOu$ zPQe*z2E-}jMcn+at&6~LUI%;)rNU=nwznnpH8U$4BME5)vs8I@s*M*wWo6b_^{mRf z(-oJHItU;wT3FXuMI87a{_@1w_ikX#c*l;mG(MBx9TX@gKbuA0H=$%ZfBr4d8=1v~ z9+&B1AyHg}^6sUVRp2+Wz9%hC#zx6ymnpT@vxxUd8DNvkycEd^?-m*vLi@u5apWri z;gC^5N%-^U&&u&&ny;B*fUZmcDIZ$po>%Z3K3&v-LcoR5?_A%}!vvIlg3#-n4E6I- z-yK!^i!<=6S9{ez_uJo^UF$+Qa`pYW$wI$94%TZg<~`_*glTnz%ITx=V9|04>z)G= zFsok+ZQc1Qw}mjkpO16bZ8F0z6PNTNcwlK`(DTRaeoW`$37jKsA3xr4=YYP#_|;u* zs6oR|bKbK~#K$=ze6ahAW(s?GSZy>pI+!v#u(BNQ5%eDGAy!YgjJ}S9ISJ?O$8_sg zyX9}c_(1;Mm5GDnM7lz>yoSa}b`B06=zcC3mJ?u{reYN)APqPXi`kJ)Q&LCbPkhl$XK0DAlRAt^;{suTGoB&A&2QK+2TKzd^OxK&+ zw>G_XGGCP04pvqB2?^SL#w2&*C3l8IL=d#{J++mtFZ>4CThQ@a1RNGdZf;^s%)^H+ z4K)751EZ&upKvv!R)Sv4a}3IGZ9fJ`rK!+7=z&nG-Ko4@Xufg0sOlUiC+El#Uk!~Y z5bGL_@vky72R8SvUAv}xzT)iq-V0^u6&$L5vSz$`)yS{qH3H4V)EU zH7vfDVFzsF29Zebw_S&;KhvK}0SOPn75b^?F74+%-~{owFJ2GH@BE=zIL+F9n3nKd zB%%W00FJ@|oQd96yn3|-kPW|v=uKh0jbVY;_Fn`c?6XKnFg#*aeFwdS2_^2;OFz$i zh0E5y_~@GW+ZSS97z{LrGkw{QzE6+6uK!0OvYNVJRHY?$q7c?$HlbD98H97`DJd8L z8&KWo+^4RP3RT}M?i}bAo$PLOQHgsC^YZfg8(2~alVpspuDZYzln#%SVB-=<+Ssr& zz8EU6PZ}tjhH6GV+Ipt~7!(L4AP2CA0Ss34#E5z(_)+m764rxeOXWHcn2t zKM=#rOf|Hw=rkUgh<iwYAo4e8a`;x_%C|xrTGu zW2t_5toFb~2z!FKw4R6t3-fg2+ZQ&rtnGu=spUq`(vY&)v``cQcgE*SI(0UzAYNr@ zR}Bx9+{(Rh44C_7{aZ=Xg4qJFdfJZfT3TBNH^-fl#eGO9C_~k?HdhArw2sQmR0X>&)K?0ivVQqYFvxz00;w{unI-K zd!CXt$BZ@OW8fy30@u#Q&i)RBX-<#=2X~gjAV1IpZ7M4zg+=c%hBe3*4yiWxkyo%u z!`%8rAhCEuOC-EIz#lqE28b6-m;61~0!-PLQ$$aPi+I6zL2uBucqn1qCc znUQKCg4k~c8ly{Z%%Q>&25=wBZFGyE4>}1j=9zMbk>h>K-V6Ua*rm3ESH+4=pk{vx zb^I6T{(ILqo=ojs_gOkd{SbOp`ktNH?SkpI(&Dp?OB?ABXW@JhU&dPwjo$udJNN|P ztO;nDb%$Eva{v6?v%KXTiJBJm(8%oS75(m{lQXviIsS2-y+4yWY@2V`1Zt0q9rwJDTPS>JA1B9mip{-QD}cw)egJ z+;^8=m*buAS%h8>`Z~M51Bv3|;){^~*oA)2+;$wP5&$NpJ8Oj`dbtv+AyRxC5&~QV zxAhBou35$1y9jkDY?`#2n;@%dVi44c#1MTUM^vt~(B!W9Fn;u$JY{#i{SJU8B#TzitJJdK?8($KX6V9Qmz zrqPPG_Yis_6F{K>^b533)GIc>Qs!8|aJ6kI;klQ##1arTx8mj=c=~oS4XguaOal;_ zl9JN$>IZJKZ#8$H3OUl>@ceo9aa77pZu7{uOk>S%hVGGpK{&Wn7I%NNn$3>|$8&hO!o*gY1FmP5;ejavA{E1doCK zL4#(HkC&G=dgrN_SGi}jUxPoE2duAlat9RPk?^Ld-`*sFH@^vh@CYkDZeSF_d)GbD z)Pm&~j{sZ>(AXIzpNiqJ?l}YK8WqV9wq@swNXQlx>GF~9veeU;TWB1|0KU_%*0}BB za?zrl9MiBd>Q~5%2kp}MLd9qcNE7Y>32p{aL9fz@3GEya-KDIx4}_m8MtyT%ymW`S zWj6lqK0+&ki5a3+CR-;#T-(_6fGjpX-c5}kE&qLr8<52dJju2ppEaT9jAcnh623`@37S zUb=>}zkaEYuG9|>(_dJ7!{FMdj+O$wVFCtmT9hVVCHsb@M_pgEOk@&4qf3Db#G4E5DTMkU3>dYteOWN@|AV!V)N23 zM!}X7*plkUgfvK`e|L7J%JeU-!SU^5H_A7M<1{N$K_}|gv^?H8!b;>e|2cJ$q596x zdmwH(1uZtZN32($hZfdtAJ4RAhQ1s9x+~wjma@H`Kp?obx1XPv*96K+9>;H&F9TrV z0f`-o{|9PpK|nSh11c`5iaV};XJ_pvcH_mEXV1bBi8P#NQ%?bECgIZ;^!(e~l^}|t zj03m%bE!3a62+z&!b&_qn6V53fSYjhDETZ2p!R_-3X)J1Y~}SWBnIspJ_{|Gm-8E3 zNZb6ryDJI=lov6<9=Pj(J;Dvl@jNi%@!7r;XCnR0T{VICC@j6!+yQj7Swh!z^9}C9 zZNtM{^xgIF29@Zn*M1vI^cX5`?sv2E8cn{!6|@>EK8v~8ohFCaoYg%LaOalx0(Dji8Z2U~bS>?__cQjaa{Nt@ z#`$~L4WStFO@dm$h8!q;0kB>Wqd-s4=x?3qK(StTE-MNU*>%OTwY58!L3Rh=W@S5@ z`l*oBx+lXmbe_g4%5LR`0pR!yiLA}$fo8Fa zfAXYS^;E24)Ac&$Jk^1sKAX56UbQK;q;}KE))hen*3@cmdI20 zg6~VfUHJv;1E4#j7yQRX3!GeFGK&h3iQmynhC9nGYTak>un4 z-j2dnp{fW_UZ0lgb~^I1vYbM)9)^V-{{=@!5csKfHe(m@onB2?y`%kVWF^?f~}&WVk? zA2W(ZRfu*z`HzzIcfMPNy<53a%wyvTL7{I+aARp|FAnPRXLEyvx|5?`OJZaKU+2~q zz6@Rm%&J(Zd1(b{V}dH{z2OIJYDU?)xrGprz(-5OQ9thV8NnR3H9~@_yT|~%YM^}mgw8?cX7%@Xou;x=% zt#wOxw5+ebVsR%kv3>vBkNSx)j*m4pmo0ulB9j|R3En82^C~oYmfV=QC}Pn)+G<>8 z6E`XL0j@&_+(ow;7fk~Lo4Vz9C)EAIXO?a&mD>%OzJPXgeH(7s{f*}r;J*-Ek;fYk zKdLxCy$CTCRq8AtpD{d)kWLX3s;k^6&i@Cnq7NR$qN>c~K$aiPZRgvAnH4F1BBc)%l8^P1Jsg} ziTN8gM{VtiaeH4~YX!_Z1p2m{aG907fM@dE-@3Is$#9_7nOG18HG3L@@OUkb3p#wA z**ws5cIJuaHjV;(&=iW3SyBe53PjOMz&nKoZWm~R{8jx>w0dy2O%hoFHCl!*j;z$Q zRu#GaVvVT&ar$G@9xHP!z9(%jo^E6v7TJrZ5eIc^r}uYy44*DHnO_%}#f3Z@9VcM+ z`x&KsC+Fv?yN|xmi4d&pTn-IidFLPS?xE7-IELqBgh4S9rvq^br3-J=VlZdVp4BV5 z8So`TKAy`ctgwFHVDH5O^x}XRO$llumJa0b#87NxFVbNL__s&ILNF@n6sLi5Rm^vD z91m74`!z%!g!f#u>0`SNO2a5Jr>(86%9*rqfYKF_Kmc$|B~4&iVMRhgASD6aR7E>$ zKV=)RK9P+=7y_@@u$;HP8a0Ov6<@U(9~(U>d;awiqVu z0x1g%q(_Z}=z0QxJR%TuakYNl-Z$a_+!EnIPepFCf_r9hId$|lXh24t!w4rbzMg;e z{2n@On{f4jxF4%@Gz8ib@da>vMQ!HCqr^9+KRn4jxCYoKQYm>r<%XmT8yDq_>V81^ z&%^a*>ZC{QjmJ-(n0^14pj|NcAAIqRLJCxeqXxZV2o}N{c8#d4r<}d?paj-pDzYJyNrNvJM6%GqUw{Np8<mXJ@zB3njkdvs6YChPqFP^ha~r;=Miz3?9;`;@dMUNFo6#%-^G4 z6d)czUs{1Pi~s`_@2q3&Up$}@Yyf>57ifvn0n<3V+lNK5qCHPjL0Ds=pixwFi}91n zXGk<4>FrI~`dgIBsCr#8SI{0RKLX&$zJ#mN2;JQ3lrVh{pS-F@vL{6#av7bu6L<3? zFP}XHgkWe2&o^a3c~uZicr~Im$)@>}5txHW4quMw1krTu86iy)1VkQLizJsjdx~uu zcy#iAJr2GTg7?pVg{)kGroDzmGNvqYhh_CYAA|GvF_~?VW&wBOaCAnIB$%-kdw>&c_r)`(euL<7 z+%q-~A$0?y$MFel9DFA9zw3FZC0tnK8kVHguN)17w;vOD)vV@;T}0yHDzuGD{o`M6 zQ)`|~q2njgM2Rt#!8Hwp^Mh-8Z(vRh6MBpqAD7_E07m8M#haL5>w{ZgW6B!KCWp79 zYq7{&p6{J#P=DsHFXv~S@Zh0FN(TQu__OlNy@``#XV0>Rg%R%-G?GT7=8?TvmEeSB;x{oc&T z`)HOg+ue2dDL6GQD@JWFl3%v+@s-Rm9!}HofPZN618md3#PS&Q_>X$QncM2(WgK8D5@+sEZtt3XB2>^ zT4si${lAW02(3dVV^B<>qHv4k2Xf*|uXehs(;3d0Z#mnYlc*|RNww$WPGt2YA>+sN zmhwus8JGuNlzWzAVv*@@ecVv}gP=9jaP9??{l`ha4QOn)%DXs1yB;6g?2x+SIH$1lU7TNanUQ??^AHmzj!gPYuv*&_onZWgq<0+*CKl0M zHq(+GW%oX-ug#NpTqcjQI|XX{(h`$4gI~G4dTlCH8buaV_#QFOr{p}V7?OAcXS!K} z78UMy)%jqot;HP*NdD^rD^3O`o0Cc_is8c+p7rd?zE9lhhM!_=i5Y>Nywo4qq=y~M z#Cz$fP6xi|m%z%?#@g)11H7{zUTqs>7Y!=Xpz|b6I3wF~?CwIF^mr7Dw+Qc5VN<2j zSkG5B17f1-l$eP|dVa@6P)A_h5oHf5Pynr9B*XjjH#YZNSXz?t%3_(vrqjj#^SZ-6 zQj;FuXgtO1U6S%)6!5z^UvI^DmY+!R;bYL45L@=VuFo5dWW>!OI^U`i*7PaUmIlp_U9-VFRTusiMnLE7{f3#S+MUcRw7WX7s zt>QJ}6wMzbRc_&LN$KucC4QG-`yPET_x^QkTICVB|JuMFtIbJ2137#qLP_}5a>=

Y{tHrm$GXpDwf3P_~P}pciglHE^?84N}|tXZf{f0s~q(4Oe(KZDD$uu zr-E>?BEBtY<7z&I7~lQA@vzj$CvNeMJbWwC6}^`%JhtR|(;`ppx!KQQ`QL1lv`Hjh zh{8`(9wdosBVf)?I3RxW`jF)O$BCyF@~gR)1+;Gf{uW`zIWK3e;Ie>;HUbHQGj~Z> z!{IccKu@T75R*={~< zHtjSvV;#)VsSZvQ`*DDJXbMD>YWfRr>CZzvwD(FU2AGXM zaB=e8KdTm=A6;Cwjg6a_vE5Z5gP)+!(`H2@EB#P+Q@m!G5_iJ1Nj~hM(tqRyYAg`{ zAx7ghh|8c<%-v8A3kxeUZ9b0B5&V$GMH8Zq%fD0**A&Uk#nl4UbR!8bl_CFY!N|uuk z7~y}Y_P`-32Y3Zt`6b;OchAOi8U_QD3?gD+r$EI?gnUh39z!0qCXojyAD|of9<HQoDjO($NeMCT6KU~?w- zRPYb-YF4gU9hZIPdUoL^hUYaszwNj}E7$9puCut-oJa73iK2}y=o=;T>SEI}-q|0X zm(^-st1dyTB;3!#xxDYezPy$&aJ0`)(h!Ta(0cx zpqm4ICq*D0vAneOz3m!6FmZuvYlQaCOR}DWW|$#6p>dLo4T=Rt{s+HOZu2ojS<^{A zu@ui}F+9-cSB=$t$1fL*!%=cCPDHBo{X8*mWMDgf>cZ5cgDKnT&K&IcL!cBi1oIiw~b1L9YLX>SF%vYPJI^ZMeq2WHXEKlj6sj2+X8@$4fSC&i zAc05qk(QY{;PD-YrseGK}0uUKRkD7LGipErd}4k{F8XPFToH;;u_hxDY+h zfa<2*pUc#e;W*q?ukf5yaxue#D@q?M`^69Tmk2Ou8afGi2A_qxRCu$@11k z*_NYXG=caT@4}w0_E6vQQ8$~$Ur{K`p*wz`l~mzIZrUwB5}v;2TCDgcOhIg7@tUfE zE-A<2S*C7gzqKj;F=fCKa>6T;u1b!rni9X+BX)q<|96sUSEd=kLk*v0{+qm`rd%dx@L~lVKbMYR^tgeGh~AGsnO`Dz4Hi3jhof@q|PJ z5}XdyE5;w0JWfz>;h+i|3xpnEBar&hkwj-k6hy^mIR>b7GngrWcHUn583-U*^`|u9 ziBJ}jq3d@y(RPgNf#FXFw2c0tkk56~MPGC@C1pbx8uMwDN8X(Lu{z&FL+s*J-*bg?FFn}8-!007q@p7GQ8E({b zu=ud+xb`)V0wo!E^Ww~Ijojm%rFsSkbnT`f*5e-%uifBCO zir_z|V|k1-jHM*du73jGSSSBhhSTs*7TTm7{f+sEiRJTy@`G&os&h)#{d^j226yA< z2s|!JYUB{xUusIRe?`e}{XL}9c!wCD2shY-k!wnh;Lo9%A^|zr3Lxo|_fBv*nFk1~ z!L7Uqj5Gp!7YnGVfHldyniK~51o|#kqAQFf4WXng%dnYmsv(iYTGXXIy2ISO1yc1g#k1+8d3)ZR3RR_ zE3GR4r)JfG*4n}gq?#aQz{a>OetiJeO&mDRvViJ=arOY=r8d{EUw`gtL`V435mFb5 z)1=4mV=%S@d2eA+Vv>>;1ot#iM;;?_nJGD-{Jl7B5~<|iX{?MEFS)DOy1#M69tGyf z{XQD2BDJP(Y@UfCLwnWDm)a!i^B<4vBwNl!72f$=u+A)R!))#?`eNFU+lAhRi?=+? z!M0ammS_5eVFFiMT|^JF_Xn;)>MLU9G)C#ujV}V(s;-?<-XxEDcSSs-;zX*dC4q(^ z&KNO%em#o~i(FT@^q=~!jmfen;|i2seo2D7m7wC?oHITM14g&HI>#JxJ*9-NccitV zZQ$TsGo&47U@NNsZ1ZbStw4xpBtPE2DRt17tqDda_#a8yMYdt5?t>689c~oT0)Zgc zX+ZzhIBb+GNGx7EPs*zx#v)T3fXTF)ny(}CjDXrfiH7k9@|TdoZSq1Y=GfW*3M#y!o<_SJos`2dInaP^o0e9j(7))f44V#3sZYTsJX zPw1-?c26&SHW$FXn*d;ciX_)Y@KKD!1s5{8jdMHf6`zn9J20$p?SyT@f>d*N@uitT=5m%` z!5hQ+bxpO1i$Kd)^Y&sd2LY4?oXA7G6O3RNCgVMO!*6| z`Hg!2iX1-E4a3QhJQI45N=V;*OV+*o==P2Q2a3TE&)3nIzYzeeXa@dnuFsC1omE|#sx-R`j()d=L@QzHL;v~Y z;54u(sdwg{g9pwc{SeaRa7+0qw> z^3-4|IoX=bk$9nc+*QJcqkIzzyx%A3dyri|f^WW2ro@!ZTF&Kev zn}>m;?&EBVg-@3}TXK8F$K|oa63??VWmmPo8cf*^y|jof!m)Y1uAZpF*&FmU{=0rb z3BBQy@jezlZSy;!QW~1|wHxUH7YP^zr{wZLp7j@+6KNu#5hQ?olTke+1!_1{tbzNj z@gf0|lOqZX21T4(Ov-JUTp5GvR#uqS%^hoED;EY;+MAX5Wc6}>U>cN1QeKZn32s)+tR!W zDf%G_n?dEg>~R@-wqTHjf&2iK1r>MWz|M;1Llj&{Vug`KL8#eKpmoN zdO1kgKx@7R0yBa}hzK!}vtEde0tpDz25DgRqXd?)*SPTx90_7M`An*Oh-4qdHN;+!*k&;pQ#DqGAf++cc7;GQ({=Js}^1 z8B6H0xLqoJOm4{+@v+Vvtj!-IQsAxYF{#!m3oDFt#n;R!iy&GwjjRaK^CJ`vU6dpVXzV|83~a35E*2X(p6_^?(m29A6R103TkkiJOC-A}^_H4LSY1Fh%UOu*3-r~4{X50?y_ zi`7&Zrw)-HDWIMLAvPQqG&}S;XiJ~+nBg*-8be@5{d=w|57El?jlHCktPmA!lwnOF z){{ahj17uONLq! zpXAuc-<1vCK=ME&tpg(-F{-6N96Uto<%iNLc#aU?9ZWz~Jeo(2ae@gQ$W{q>z|+fj zi>qnC+k1dKiOA5)0C-_@PI{7aKQdM z2!6G+miNG>$Al-3^rAH@^6SD;$RrAKNQV*wOr0~yVxDVigCG%7 zE~B~-D%ZaR^@JkJxf#AL@u3ihPe-hhW&%4|G%18jNV6{EtwyWG9evwjnZnDiN2r4l zeGssqvtiTFF`IHrqUcheZJd;p`86kVGm-!;JyOjZ29tT*HhHv6-_vbEq*Jdn#STUZ zkoX39{iw1MZzi|@f3@!E_3obCYf55O4pd)WD%b>IdDP%Oe1_LO5ei)yRS-90Tpv** z#x(G^d>HeeGF_2{iI0yARBdp@9UK;YHSczS*t>=NrEh(GJ0(PYEk|DMW>D3yfH1Z4 z869ZMkdhXr5FoS@CXY~D&@aZ}v;5cqnp~}PE?9hXzvp+qCxEk#4zjMT_f?OpSBa1s zz#9_kMFJTym&wPPSEWESfQ2#kUb#OR7%I9{Lk5zHOYnGrd%!3KR=5pzn%=jWpc_Ky zZMFXRx8=4gAd7gNxOwvNQ%QGW^ zPJT{(CB1}CZ!#x*iD1DgzdDX34*!$lZBq-It~6m@(Ka2k$s}tC5uB; z&EuK{kp=1o&j#Xpa&BeBl&xME)Zgy&(Mo?s4eob5Rm1yTHAV#QRCm+TE&JrdVxS^0 zNWG4(N)WBhCD+*?xCHJcrmazQXaT-{QV00EW2Dr$%6+MzCs?N$Hc+tV-rQqs=u z0u*E2rPi7#HUbXU40Dacz}IsV{Kkt2=~K+tulE&$%>>tY7fjWF=u+_@5W1gMS1Ve$()!WBjV1;^*;Laf?4bUEAHFRvkZGCDPfW1 zU#}pwM2vhBj4SX*VrPhNn}B*FSu4288R0C0up1(+ZR9x}8ySe#V0IdFKm~Un4Zj1_ zyGSkPfbzGmF6ywsa`BE|6&ZBVD3vd=JU|GzQfoVi>w#4k^hAUhR84^EfYuC!XBe>o z^Z+(SLqh}E5ZI~%--5giYHNcsoBLpZsH}#mZn`PZg(Ev3sJc#IHUZZMJe}yTMPrdk zc>1ws445vUX>L6~076V$z;2AH?!1K@PHP4)h2~$t>|1W*4q`iWY|D`h9SIT#AxbsD z(4BBdKQIS|80nF>X)2*d1T&KVrjfA8I~<^eFwB$vNTv>*2)i+!w9g(KsH?Rwi;WnD zXw1x<>qZKUsUk*}d6~gX-%_RJw=8_*Svc6RlcIA0dCd}W32)h(Q~-)YPCLT%-~q=NTLnUXkCS7Y|5=dZdLzrUQ|*n4NtR0P|--Q`+oYBl*XY1;6k zN=?^7=~b#5zTKr(W^(mLUpz02#`}SzNyN&n{oBA~XsYR#fvOw!s|6wn7XelKg-I+6 zmon|mk-!Nhd=N?g@(&vt({cr(DjLXg9bzy-D!Sq5qN@V;0}i}E=Kg>+Oem|qqOPSC z1jdT0XSv>MAsKLi0IG5oB7>V>FN#bl-#4(L&&XPom6wO6%`SsUT_F-8-8&d18jMWn5L02(f=$MP$gVa3T#o#hy{YM` zcag#nfD+j3aRj)7qd$8a$@C+r_;BHepah*2SwYG}xcR6BgdTqkWNzB}oJg>C{qA%e zcsbOg*$`|jl%@cE-VnOmhH!cz@6_-5VWU;-tjt~Vm`0K zS788XuuRhrDxD_*5=Jx>rU4Rr-z@?-K8>JNo||7nWesvGh%W)u(1gkYh-}EUAP==j z+;n)q00d5ZaTDrRHo_QiORWAiUU7d-&UA%5nWnouAf9vA;j)J49 z9^5^{t&I6b1oxcX;=_b}{v}5-$0zL|p7(n0-)_36Tn!x3>49e0R;!G5if-RxXN39@~_ptboPf!Jpe9SO{>BQ*^ z2;zl_NMUg8k?G&&7Z&C#7|=m$iN)x9tH8LP9_yIWBt8-qO}*y~2NZ zI7%}@YTIlk6y!hW{y0@+!^v|GJI+Efx6BStXW$;2qg(J1fp-qUNi_|Ny&)V-<;15x z@Twi;Z93rfL|eJEy5E1j8>-{(O?B;~LcafSBwPPymdDqo4XyeHKF70~3FBBd=gZZ_ z8}(FNCl)IgSL)3f__!}P^kVwb+-p}V?HT8pfiI=5{X79prI?=K7RFR-|38i6>Z=I| z)yR`R@(JS)9u}mLABg}ehJ}Zxdn6A?R5Wl3c`{(|ohY)4cV%25TMv#gIV|O2de1UA zl21UPd;**&deeNPppuN)=7k@ig*mRsiC^S0tpqJyZXGyB>_+S8P~p`Ofb9%*m}m>k z_(6n0L(nm@KkZzqud)5tN>bz`L}Mf_Lp8PuyNI?%)FKy1;0M>UPU1UA5D|Fad-L@AVzrL_l99cNNo#uI~tIJ%QghEd5HOw zIN0R_J!v1F01|8?8U_u!6oZtBFEZcO(Q!DA3gn_UfenTGfVt_jUkqFm_(r|Z3&4vK zVZgkrTjqCd@c#DpFVEU#k4k|tNuD$bflIqqz0-PjyRHwa6G&X1p8O%y#r`%#$AsZB z{Z{gMb(&%~!k&`^inbw<-bSBO?O#5L$$?O|9 z$)96Gag2g?rObDW!Amo!%tpqJi)Y@~-AGwMAYvjN>OL6T?F_8|GI0XGu2 zwA5kWaXi+W@;}{X3Abz6KIJ!bd}?m{z**>w$IdjhocdRp8Y95~is*N!D}rfj?i84t za62h^OlA8**pbf!7Ayj&GKFe$SOuszIM4>gU9s~6-2|GahCoq-@Ge5Bk=FfhCEKnD zf{0PIfT&Uk4!7MOHTuik<$k|#4(b{?gFK2P&0Q>yd;eyGf$@!x;#~iRZcJ#A8oV4NmKSFWxZ-%j^&(M zlNEu@-0MGZOPe_~%v5MesP1%U)KZ5Fm=4=BW@GkSkc? z4dpbG*5w)4^qKuzF(>Suqe7A>Z*6a&tCag9R(hblGN`tvw z0Cz<|&)7LKJyu@bV%A=&@A3P7!c70Gd`&+WOZr=+ggna**PsO_w>ifm7@NXv0J~ae=tL7^oa@`Te2gb6o@z*Y zFr#WSg=j(mx#=w%wS<#X5(?g*2Iv5;GMbZ%=#y>Rbi4ea;>OhF(z3oEY2H=*{~wH@ z-5cp>cBN3OheXp~Ekf4IOvdJ@YdAN6Ho{3p_w?S^XJ+e4$SboFgQU}s>bZuL%O zU2AfL-^dnYm8%5q+ne|ps6b9WdgwgxhGOjdmn8*n;kRJr{GsX{Kmz#~8TnifhD&F6VrkbxA7i_9;Kb>_jZ>8}5`<`DBD8vOWPno?H=*@d@~Y<)1{_|hk@4e za6SagO@!uPgIY)fWOU@veN|WIc7O*L8zx`jfItFloM>|2WoRPMfT=HiQl6fdaEa;X z9dI%HjtNnf!gD-~(4sdDaEdsP;m9x{4-LO#ch89S*@ft~Cb=iq1Ju}BMqbXu3>~E~ zzQk*@H_I3Q=QzU3h7cV}Sr#|Na4)fLb)O%K)~olvQLJQtlj?o%f#+$=P~Is_N5m2K z5B~A?k`_Wm;gl9bY_0D?Dz(bu?@LDZm1PTEUf+y|4vX`{y|BVNSzSGkv^o5AXcfGv zWWxHwY8{xv#Qk0>kxzff>8C!QpGZ%o*{77Ts1n(=ZSgX{qVb7xv;dw_MPKI7F)^2` z%WLxAkiaWX(5=c}|KQ-T5;R{r(v|e9-tJ-w~qu zz8W2eagxwd!#K$!P&X_(;6cj)oQ0c^0{ZfE_|yjE4MlAdrlIGhvNNpwBq*Mmfzg_$ zPlMqp&^6Dtx|ceP5Cx=^y?345F0;8^n4$kGzH*^pW#)OmYW0)C1isGg6DLnT!eCOG-7PfgjIE44>W1 z>&(x8jIBeD;pFgF;kD*(Z&vM4<^l9;9AJ8Z9qV4PGuP-&2cp2RH^5)=QL{WZBu)R=tb(uC*|5 z^W%$kY9(c*B8N}x7p^Zefk_O*D9A-WQng_h^ZM^fLNQl*hy%E@oVBvgP>kMq|NIEE zvcFNEwO@j|WT(>2GQBbt=COB57Z_Jb+6*lCV}?$c|HjG>wL9ubJ}tw_OR{3_?V1`i zlR!Ejaco5^I*@s~g#KF!fbd-6WuDvFr7tD!+oxe+ASaS_4FltX9XIo|J2ql}HS_ z>=uucpxr=zoKa%wLS?-9(0R>+`EEd5 zy`)W%Kn}eKu`cbpL2`7T-O*B?Y+;udZkMTy2mcpg(o7vr!Cu~zVoOm-XHfb&ZM0y- zbKbSvRX7}!4o05ZoMvAJG(zVq=xlClR5eEk)JAzqRLMLLKV#9v$U5ZMe3(eBWh1Bs zjz^3ArtInIfddD8GPsO65Mvhm=+Pt$qk<-K!JV(U%JnyENFtI1j;MwKhMMupkmlSO zRgK=h@AqrI)_#67;u#VAOwr^glvM&p4ojIe-{u~cYwg`$c?Cb;eR1;Brcb`Y zhk_rhU6Ga2I-N7;Bn`L!VSs1U|KxOFuI?kx6kgJrFas|cwjzON^D$r1Isdp+40Doy zmI=dFMafaC1Wc)EWLJw4i+dd=;TV>IAP#9OuX}5Sh46%OVQ`xoMqgRx{Kp(Qa#F)S zPw$tFQRlhyod+lcbfQRpsh7#5cAc4Mvu&lumv~ou+lW8eG_0KdLw2T=NEglrQkSB> zn*u|(UGifa@7K0;kBZh>dA_7KfRDf%z>W@D5=J@ zGyvp=-1tz^RaTQXH#a}!w?2txJAh@?eZzMM*q`d3?x{YlSMi&hj$j0T>;_A7&IQ85 zDGNVsGTeBvoj0s#NT#asEN7=Zr|rW?_&aH@$cf2DfN74*iu^~KIgQc`t(HOSP8ZYL zXv*aAj}D9_N88f(x@@vaG4JGG>{A%^)Z}c4$(_c!`yZ8J!RC>Kw<#@~2A4(aL_Y+0 zoGoFNZFUcKs~i|c~#PY4ri&&+ybFz%wy6(Cq9QPRPnZ@{y+p1 zOwcx(lt=Ifn!HpAHqH^FGE2Z808If(23nyJRXi0KHv$twXzlI4&|yWlwg+!~!M*i6 z{NcpO3r4FD?sl$S*t9B0F9btu{wFF72rYs^r!2fZ4esc^TZsAoQ{fPC9u|UDr6+c} zd3gL&2F3Fo>1Hdg9(*9%_f&tiXCaRjODfrk&3F=%HIIGC?KcM1h34{DVs-7(jHM#! zRx1l0QJbD2ZAvMoZFa}iR|E{*;Uu}`Q}AezF-?A1xiIP(`>Sg}BRPEaXHQn^V{SW( zD#jgNrSN6W>g%>=);~MkiHs-qB1)SP*S;{WDO@~#Tr7u;DecOo$G4x*{nj>6L}2UM>Wt*BUn{YO19=XfLp4br9&AU2MFl(>k**(&z+NG3_Z=sx$|(s)K0Hnz63Y3UYM#=Y|rm= zt?jGyq<l7mhL?4G?{HZ$akT zMaEv7nIY`?3rBRkZ>iJYmmqDTGLF9WEX$)x{-tco;P>U~`MaE6 zf#B4a@_}`mNA92A4`8P%%$C_2&H3I@W}s(e5(^4iqx34lL)+Z8B#S0|s66$DA!hDR z5CVlt5`^W#0+oO^Xa1}HUgmw64KWTbPhvYA+S!;CGDkeu6VwoM?Gr2pw| zjurFCfb?7QMNuPv*KGJeck>f5>_L$(7raB2wRL0fW2ywL=R+Nj-D%Rj^w>r<%%yDeC=ZP49M>tFnD! zQWwh)^SS;zA3i&Xb==f=ZZwB4TE<#5sN^Q|W+d4G`xvK;Vq56_ap&g4YakMeB*UP= zzAePza{D$9puX zTc51-Rq6YvC!8!LZGW}O$!}w8r~K&O2ArRU1tIi?4AUKJjF;b^=YTK-089k?n(&=D z`{AnDP{GzjCC!l2xz}ccl`EEyU_u?~OL?_C-lQIuV|dBXk`eu%I)424*H2+EZ9RfPf*xj?Qtkh)s0Rf+d~gCbfOvR+2eqUad_n`@05GJp zUl28aw*;T%?E9tiTemI)(1HE7(7jDfu}1lpQN20-j~?MLc=O|nH#U%tV}^tGCi?1x zXSlCUe(2pE829UEkNWjRF6lRgBip5`8-|?aOYGlnmtGIQj8m+WfF(;nI;x%_$QicE z^n+>Q{pPInv2H$=(24C^%@o+K|4Ur6u&Lfm?MVFkMwWAxyjZ57LWpJldulUdJ@J>0 z)?7XcbKT^jf^$m4?x@_?y-aI)P-7Ziyr@M%hwMn^Enf&}vl-;c~U z{P;Bvg5!69C5J*gYXYGjG_jY5D;x_)0_96UJqZ>ZRU?|<0G<3VM=$`5zBU=L`BddQ z6Mce*0Q*MaV0ZG<#}Sp4MO84MPT|H4F1618UB+g7^B_#|`5YqXEY3OSpG7xx^txQq zH;VHl<}{4Mc?Mhmhqbp3i?ZL^hDSg;q(d5&lI})YQY4)b5D=u1ZV>4frMr9Rp&3vq zLApU2q`UjOhWokiz3=_)XMfN4zRw?UFu*a#am_E*xz;+*b3NEjsy8VfLecdaiV9aZ z@mwFopX;N1Gr6`O8?v`LQsBNU`r2;9(o6hhl`iYb|56dHWZh{5He_vH{yo@^UiuL^c=3G!r7fg03?z92f4#i2bN>z~bnW^XlC4c%3L?Dd zftt%Wr^(b)5~8Fj)kXJY0>(UKAc(asP7`@zpch#~ zqaQvp9qGr|vC0nxgjV0&QU@*~4hL_-7R+u}R?K)-rE;#W&B%bUXS>LW%x3356U}P( zxLJQ+8JgXg`J5$9`=}`zUFJc&TdKES+KQAarib|SN==`C|FlPX=^Y?L*lZ8T#zADD zsrJjkC$rrOnUR}e`q!E*-MlYG*S*|E43?2U$=_^S-K()|M&CWrC7xqpG5A1ja0UzP z+Pb%MR)a0?xjtHcGf`tzt^fG{3m>kYwvJ^S0mD9+^UO9YQYClvLw7-Zk zj#CJ|p0K36ZXOQH)7cnw>X~2{dl|-4P^^BuZj7s0J5sPt3_HS~6GdH!0Ppol*yja2CH~Y&00>KW%H9vV8-6)%?``+7ktW2;|6t)dX}G+B{jJTxXm+EIq3ZI1m>s6W|>fPRy0cagw!l9{!&1+-OPAd2Fitn;?^vr(9+@M0Y+gJhQ`*D7?%elL~eB$B$sO%(K+e{g&jne`3n z>T1{pha1Qny1wRja(N9@_1gjWM*Aj@e%o3!>B5Ejcf0F?g}RfXFs{q(#Dannne-)9 z$Z|;7dO-5 zD2=&vo_#k}qHU?4e9wlb=qt;Jrfrg1H#^pPvGRCAH5!DTMNZbf9=$pGK^f47``JvzoV-cjj*O&V&|d~sF%X)(Z>&FGsVb_ z`!zPs{=D52Ti)$K1_A!gMl-GKr@PGpgsW^hQ+TC||oDZ2{6O z|8(y&5}IC*@e1-XKt<*->qgbWZV(9kAu_c=uwzUHxUiZoPn3yBi8IZ?4%KGnwn?tW zzm;?w-R~#m@VA@jyw2tar#Q^EuYDfHnXa+vs4o0M;b=??90~inB}@FM<>;AWHnv-b zCm+FqfeoJ=-2|#{H|!tj&GydR2L-RsAl;0GFwIrtUCy3q!E~(HBzIU_K+))ns^(BH zNp(I9X_4=Q2XrnS_C!97TCn)5YUFmM+E&6N`iVje5N+N&hqLJ?nZT9fe0}*)CbYc+ zZMY5CrgD=>&FIxCQd<~M5Xd)@SyC2-I2qcK%zCJZ&&=c}z>IF7v3VHOivI&W9cV($ zS<5cO5_uP;SOwb3au)2mXvPf=eRrkVhFPGthL%V%> zZ!X`bo$PvSyb(QW5h8P6!69=TM1SqF`-RMNP=UC!kBD|rXb}t8PN8L;RVr@11i+ng(1QZ0n}#D$lUQ5Kv0=^Auzel&z-YKc zKpk;8No^GKt6`0Ef^a7J-)lr+_Z^ zISVMGt`5D$+Ukrbw^+w?2}XV^{rq`Fqf!M>l5FB%^4Yl2d6|yDaJcLyxw)VZfH~|A zQrm9kttc0@j)AxMtn2Y(7ncZN>j11b06)@k3CY6Z<6`EaEtpk0;o0(#UAK$F^im z(wy+ps!x~9z@i0e$+?dhI&Lyy-ieTC}9oHAibOgGm8__@!d63kn?22 z@wOMQ_uQ%puMWh(*JSV|%@!w?Iq`N5o_oPxZWdwokJN5AUxD6MY5Thz2|0ZVHh1VV z$>vT1E{-8;T=I1oSsU*{;GMz3NEEEAHPL@Ty%{O^3_d+2PZQzwH4EU-i525c$rzkfztG$&k-?EkFA` z-J8QGfLGrUEl=e)czYJVRPpdQS1Z{WJ@6yq*QHd`R9?VdGyW>?a1VssM;6|qFHGQ_ zPD9fy@o?_x&5)rIVms5`Uc+= zHnJ!6+0b_o$kymV+8Kz&K?&GNWSKT9_`~^+LrGQyD~&#FaE&ZXe^2-)8!a^7kqlC* z@7`^fqMWe)tb60{1~v#ygKvN8-qr)j&l6>`z!?f_gKJB#6zW^}>a5J%jUVhD$)?WD z$9Ekw9}M7nC}gH1g7Vnc4N1;HHYUcHL0O#xH&q)4I{9Z)maK4Q@$hkNsgAO~bikAc zc2g_hLI4+N#veoWdUu( z6l14Szuo~)WCuD`TlQ4s&g~R470TS?(Z~Fb#Zm&`Mb4jgUI<{g_h05-pMBq@208TI z7Q6qF1C|y5vd>O9Bo=xC4vT*h*o_I1&NpFkv`_J|?(bdKid=lmd~lKao+a>Vh5pf_ zCksKlar2Y;)FIvCZ^=+?&c?;orl4q`H?jb{049jU#0sf3AUM4_7D=;iqTl?YByTP) z@u@vgPT+fAy4PNierDedjIoUDNL%%m-06{MY#cn8`qSw} z*~H=2soBZE4|FH%P^ds(nZ1UB@yd%?C2j--V(ig{nAt;w!F1exkh_U)nMu5pdHPsT z$qKnwu{5#Ml))DN`;;A0-xynbLJ)sxUkwNEl#?*QD)z&GoF|3O{2=C+950he+TXX; z0_2as3InUU0yEKYe(toHk~js|+D>7A=25UIic3Yuv~&rnMM^2)RMR_{`O31gV}dkJ zZ5@)NG&ktZdq1nU;j=S$ATW3w$1Zn0`eX`AJ;k{OVCBx#=IN}Jdn$?KQ){srj6$<* z*o-aRL1#vu!It>QK27};cf1H-X#g*zZ)gth47Yl-_ZKdA-<+1FqN9E^^O1SdCw?j= zde!z;+0}?UQgp-YloQm7PbF*!H2fnEejb)B4z*lUVT3%Ns060@B#ZM|Fx-D*!ZcOx zjRSPD0G0gCtQf$qbx1~3N^>v)cQ}zwpOu|bV)N6q4?rebs@TmpwtESzzH@c{jK@d5 zR>=3xjRX7zSlc)~b@UbYGIn}IuTGzA-`wvv(<+DRiI9(VH`WitH_<_oKU}6?z=ddT z!clTwj}J$~=+`S5@Vl(C-0ri~ki6;iGoS-Ns>;nER>d`Ux$T(ZmfML30=_1sv3?nU{VG3={4*UY?$-#K4N=eLwf zLqe&j40`1z5XFmxD2?om=)?Ez>Sf30f@5ljh|c>I(3;b$4dpOWpc*A9lu(Q+Rllei z8wky8*QRjXUW7^ip^b$^HM2M1z8l<%X;vyLG666FX7qK9w6&^YZ9+xfxcazaZSGfG zhK}`$tRpZ?Vm4^Plxq+5yf_JgGJ)WYE|dV&9NHh2!TqOgu7HuT8%Cny3;IuQHh74L zmJ$7*aQKZjrOP+w9YcUACJA)TvbTDA+PjH+xDI0rUFS=ai+)<&1HIB~%)T|(j?)_E zwNu_*Dhj_d+GE72h(HnmYxwE+VHytcVC#8ra zeRUDfkaES^a?HnQ52i&?1LfuJmKE_0xYYqrj{x+{U((xJN?Qo#Jgwf1-=>-?0C@eS^!wQi5ml>(812(-YKmp9_2vpJVbXZ;3#CUP^N4 z867I{?k}luV9`h*=j%BFD>6Y1;`6o0U-3T9)w>rGoBIUB(^z`cR-T1idyi1w#OY+& zyHqqn47KG-kmF=mm~O4p8y}h!pR_)O`ZF@;Oks)x13?6S%CCpc?D_klJ1O}soEn{K zD5=*mp#}CMS4mMmfd=lio}fEheDtc)$u@e2zo=%5rYTpxnz;L7Z{K~(?YBq5)Ga8H&_erP##QQ`59& zMZGibJ6cVN_~^g3+EZk`puldj7aE?I0h}>@ZIp(+GZc*D{Zw=0 zQwv}hR(Tfur0Cym-Dw%JgFKH55L%a&u9*2?=0UlQKMg1fE4j@ma$)9r%+Sc6xHkW` z;|6_~{eFX_84O*ITcC0tlT{w3HAI0{RhzQ?n)xuH z%XRzx^u=q<;DiZ5pu!KR`pJMP1UItrnQmc@C3?+Bwm&^F%33by5q-+GoVVDGS1c&f zo-`kTceNV^%qKUb+*HLuv;*veGxya}Xwq~gehw*W?i70S$e!C?K46BTcv2VF?OO!t z@F~XcwjuKQ>*vGeJO1(e(*w1Ls}p7F@jZM(lj765Af~2}XOB|mFjkTe)j3^oK&@Yh zq)hY+lQh~RrDQOBsFDSPWr^n=OQBbolOJTNlo+HIxTFk_GVW*U9QZerm8mX;0p;^` zkL_^yXlAJJHb>-|i0Ua;6nBYedl}Da><`_&9c&jKz`dg1n~d9v2sLvl5K%l{qz*S}BLA@SV!5cGG*5KKSykQA{|8f0*E>{bSdfkkEX~>VZm9vqLVrN}?`sX>tiv zQZ;?RNO-*P&9n-t)QeT~269D!C!)R%KVSy2nuU|K92w)?NdC&wI#v4Qc4O!wz2?h?1 z@r5PH^$BLId~88NQ2Bgzy>2r%4cu=P?`4b{qt#Z65LUO3s${?wA6O zj=}pBW;CRdimhnD60}giL#>n=9fOqo%>}{Hvhz>*r`%4Lb4! z%nOU9yT{J4MPuO<&7UOsh@Yx_r4}|(X%`V}+xcZb)|Od?Cwf1I-@JspsQw53BJ8!q zg6u@s4-5hIeHd(f36b^I#qGyLkQGmNzu)ZZsKulLbT|f%VF=3?^IU#Z^%b#282u{)u_=DU$A^=BV~2&Pheb&| zZ4Zx6cS(Np{elniucp0_ET`!c-IMRNOe{q==Ua%doF1U?jJ#W0r`Zq$>ek+s&B^#V z<0%;f)V4hGZ~Q-}JJ$h%W>fRWKR+wBQBD(Zb@Iwzk#9fnaTj?MwT5djtQ*5V?JIVB zPGTAm`-25oW`%TnlF#gYQv0EF`fU@YBD4w0Of^43VEeAoPe zu*R3{3b2VyqtJ4DjXX${NZj!3Say(xJC2H4to!53Ve3t;I!Dc7oiTe?oK3UwIAf(2 zGsiWTjs3u;4;V4KthEssbjNxBE*A#9c%|;U3xDMgjC-TygE1Q|Ej|bhc}DvHsP9kI zIPsXah!K{lCV#{P5U?A}mU2FXn>q}aR{TCS%+53nj`ZU{vA@{k4 z|96bDg~rD=JcuYyNslOMO%{ovzs+l%W4U+p+m5q`CHnTO@D}CiWBT@!YpMC*w{58t zm<8b(^L|biJOQuppDpA+G<3yAnR@aq9Y0V&$iI6#VB&C&royFL4J?ykrB}*h8s?fo z|M4Tpq*3Pw!>rY4?Egale#2GAso^*^O-NOh+&UMZgfINrkA!+7Y;WBq;(dMVfU zTZpsXz6&9Hj$X*|chm8|zXB}Chn8#rKfC(pCmzxG2d`)_lAkEQT9z30H&*H1PHI1KcD?*u_ArcST zJ{e}0$oqCZFFea4D7A{x7G;H-yainUFbN$9BTLRh!9SIbCJbej-OMAZ-~|=-{<>;f zB1D)f^pQ=@-gs6xicv+@|B26sJ3}9%$J_iesua@UydDisd8QXpq(rQdU3( zm(k6I)bh(bY9xe{aY(61@131lru_ZMm*2Fi83GS}^a;=WS(y`E9Q8l-A6Z!a{)?Tr(X};?ASnMKiwDm% zKm+qS3#ZMkN8R}`^KAx;N0V&GX91FRhF+2+tqyy%Gg_#TKC$90gU z!!|uwhw&OVeEdH3FVwwP+&7Lu-OoH$3q6{HnVjYBAsBMv1!dXQAy~*D`X|4Co^g1C z4-%&-LGKUdvAEt&m}O4m%ZAx@0%CEIHc`4UmA=lT7BPdKU(McAQ(dl=uUL(8o=A1n zF1>;u)30VibHzzlh5WdAH10>JRvu^X+^6Ulgu8FD2BjLYsjWM z>__A8x0}8L498QjQYVQ9Cz0VX>-LeOP$6MQ)@#L2Ls-N9o$qK9C_XZ_8m#`668@3p z<6ua5(fQMgElXRpGDbCp5$+$fwRnA=6@FmQZtij1?$$sZsc~&oa`5=BSHYd<_~@fx zC8*03ihys1Z}nJ4IkAz$e)o5zSWurvCwva2`!1KQXDT3>{PDA?(n|oiS;L(HK`-^( zC)j{R#AmQ<1wek|r}4TjxeXz+nAtKaOAgNBevD4l06Q=l9qKy(NVdkI;E<8jLVR#b zNlt$OpMB$iw`m)hQnD%IQJ2u9J0GCVf<*Bo5WO20{-z4W*n)kYd0}&!4`GRh=h(7| z=GX+mNEGfPVBVpygS?$WXYo}NgvS3zvW9G`W0aw0kh&grk8F_(Rs4)WwNY6{vIfD%S|-! zwLOmK%?Bza#j3}Jh{`K|;MA|9`l1>pgkE*Xhfd7v$L1Q@dMv#iWgXJ?B|H`tQ^Q7dbYX`?tiM!ac0HbIwlyQ%ZGQ<%JhxocPK( zU;8_~{`C+eMlp1yCjeKpdsoy4`Kx@%v>4L->lJfPkz`-z3xggc+CT+TAS)ZpyF1Kr zSlc7b!3<$9Y3~wfCF`MD(ZaIK>K_&Eo@7i0|F0+kR*D;(MxZ8EDp98I zTojI4Ph`Zr@je#Pk%`}c5ngS7lsxYCw>kP*Ar_sF5g64=;n-tKV~^hB-Uzd2B5`Xs zea1VC@~ZoSYNL?vFv8QwEu_+{&Y1n>A6|*~B~R(5_#Apj7GuQypA-rx)@mU@rD=bH z|K~+14=&!o*W8$5YJ%U%)znaF7uP7XqEdnYavh16c;xZ0=bvQ#ju$YiVMk3!&JB}T zgWUQwn23Zkp)<2vvi#1_q2hJziX7UWXsdD5Z|Q6DoP1`z(SLhZz>N$GpkpqpDSqw`i+h$X zD8`}sRU~R4OnPrIr%6<)tP;a44+zHxkD3qvBut$$dy2t@uXja1*>;Y16rE|SlY@@V zP&s|Z9DgS6rwpv;$QGZF+5g}(8>@`4H}N3S$FXF)qt+|`>?L?&w7Hq@9^*Jj3zoIU zsqE~=Ke+sTb7Q_w3vlcIC)}y6o7w06Qkl^ImOb@79=#YaUI`3->tbO)iUmRimicV) zS_=P<+v~5zzMG-al^#R#jKioY&`a1L^;fNGM^E~r0#b&m@(+4L`{h{$~O7Bt#9zBswh zhMX(e!%n-TWL@;x=1AAw(_{n8V-C>exk=5`kCZWrw89uXf9!^Y=Kj+x0zg?=g8iqm z60n$^{+qCpB8fWRCLgA?ImwjLAX}zw*Cvl40aV`I`p%uLD zE-(LT^os%RWzzgv*zop7b+h-7fruVnk9O)nEydIP%Fm@TPZxR*=pt8(OVAF2o3&+# z0R6BCe2C7d{>4;V0KiRQT-u$gA$KsnB25-3wQIcBwj_>LfQ=s@71Wi@hw<+XSBihU`4nGup~3WeM5RwL@hn z%*{ni21Bh=FAUGlS=0W0sB=p9zK}@Wyye9^8)(}lX8heA;(V#wq_@oMX*a8v+EfNC z|3y}6Qhc^d{)(zdGly9Bmz#eVrc15vy$mopoc;MD;qrngbMgniUW4%+_DrzqhqS3r zRi97ov-w5>Pi|rv@mW~r-^J;EgF*t?^GXYtoMLgFdHd6!Jy{*vIO;|t09xX+F zZiU%%Fi&4d^%~28OJ)?@NC8c6AeB3CgnKvWLVk=G^m8@>8k4(P3)kb8s6Mk(+Kdf17C|`GN!tT)5@W9 z8P$2wjkh)19Hbl1ILcp41+%Y%M~5_NiaT{S^V|Ov?Ln9!|8rAD!ELJ``<(_`m9hcD z^EhW!u_A4F5k!6l+%a}!=Ik6KgIA)W$wN#%AF=?^ApOz1^>gS46 z)8ZMoVahmDmd*V{uTYMj-gB-2trogl);TZEM(ed|9VKw537Xi#ChguvQ5m#-#>^oh zZef|}o7g!s@D{`Zp4y5dN$#bh25`RB&4%|C7Wxa-**_euL}u%69@>?p|5j3#SC&n# zG6Brl{>6sy9KuySq3Bnt*W_$A*b(a8=)74mE<3*P6gbr`>sqjIYlsqE<(zXt@AGvM zp0Ok04e)vm?}TBSpqO?HptucveE(JC+eZEuXZ9N>jA#oHr;tMgWBMN#IXWE-F&XUq z%Yf*-bl0Z(>Viog(&kZ<;0$gjzPnwwUYY~xyLB!8d%c?8>avb?s>j93$Xcnh<56Rc zOFf7z>06_ic6)7THz=c%)Ei32?eT%nxv#i?VffS(^IEmxSQuZL9gZqqX3$oPA_Glt zigO_1*Cx{e7(+z9p3Jbq3VRu+ISDrE|5+C?vl9yOulx@@=YNURkdPAWR>D2{Pv8Du z>x_or$Q>}!wy(-oeb7nKtyS%Z+-h{LK!>-Bg89uE)Mmq+SL8>&wU)x1`M?rR9O27_ zo(mJDK*J3tEZvoZE~M^qLvh~#raxpm^=l^J-=!RUVc{ZyHmKC zWMr95-Y<%@Z5aVu>ww9Qy&O7+Q*;xts+oY(Jc{r_SU#0$f64WDN}*YUZ!;}bol^W* z8MD&h&Rp)lPohuy8z8l7qZTT)*t_Kp@&oKzgWD-xPDhW?7L?Ed6M}y^(>QlG8B)@H z&pnUo;fwOfA3b0Rk}}mc2tYl_-!z;oBD~P{bF|PJiVbFKdL`br?-_e6*)|duZ1j%k zeW#AF%u`au-A!i@`FI0v*B-RCMkql%4OxfpPsXc*{cR^#zzl_*&e@jLo)Zoa zWKV&+F#TIJo6efLEC#;HP-aTwR(Z=*!^)JdbL4_lG*l-&o#bUxIaJna^CdYLIOLeb zx~d~%#*pa-F>$=BA3s$QoqI}~*FL^@B6DG1`|2yQ+Fq?(yILhuMU8j1&(|?DZ-`4D z{AA;rII=-%a5*aCFGH)#@BwwYNSsC8hK6x*x)@Q!wM-K9CQndMe0;{(kv*TR%A`Al zaevOSrhAAz>uu&6a}vy!?NGd%X_Ea4A9$@uAN2rH?8ClRX!i7-f&L$d?Iqfc=Po<2 z+pO?HQAQ)773mQ0>NQeLeqA z<_F1TDUqL7bOk)%HQt8Mv3@aw|H|xjEOV--97g`9wrKxd6H2j#Am^wk%kxJ?lFh>R zc*-}<2zK5wr}RxX$5a03DxR$F-&FXmyml699J6kZP~RON)(6Si^}0P1b%9WR(=+jM{J=nXu? z@kL|o-SyK+nW$C0=ovm-eBG3p`f)rAkw`n#dex~-7b9$c>C`$QJJn}H*`n(l#)|Uv z!3O-!sEal=gB{gxtP`e+>dMd^I(l!E3v^( zm8ddD-5UDORN4)L`ij=txlJK2=mWt5jsJV1xk8zX&v4k@rv|DqFfFn2Fl(50$TOhV zu;)H+{Ku!d_Z^g$@X9y4jOpv34$;yYYv|xoD1FXt?gJe)rh9MRY^^-wef~)Rv(@QV zCtQ1V=et@lt5)(H>kgGS8fKVE%5i_H;K4VxiJIxL0oq{mrhRqshLkE2y}%OLRXWQ- zlP(HJaY)?g88DfZ>1kx0{H@SgD!l*Cc+r)94}pB~ltcT&sy~bi*OjW4^l0omThmMr zS=eppr8?{6FKug>Mh@#YU_tdF#?)r87~F*C>~luTQ>R~nQs`pp9}ZJkPMU3udTtNj zoPhAvgh>ostj=*9^u2H)-Vj!+RP6IjKEb75EkfgJXD=%J?a%x50UtK#A6Q7Gcs4U7 zSg2d*J6K*m55?O5pwM$Oks!s{89Rs;S_kVIuT;8-+sy17lQ#$JNjM;6!T-?X^`+kn z_|qEU@5Kx4mHvV@B3zU09a3E`>zSJzP}726`qZ7c1Si3`=1P8<@8)gZ3g%$RjMK8SV3ipE9U?;)3JY879zs?wa)aJtJ_Tj|&4 z?m5jTQbhTw+b{a`PND0>!cXUhfNW^ATgPd$0Mav&MBUrB^J&c&7Gjb2O}g499nIS` zA0ev7eK=C)+1dOdq94m23H1Qe_ZQM7;n6#rkgs_zgagglq;_1WrV=C_%{m=OoO|8K zhFj-#7^o*Onp-EwJiM|9wbs(8WTXC%d1uGRnpf9NWd0xf>K1g7Yq6s`H(vFYyBI1h zvs;BkrCW!3&-iWVqk$HUuz#i|1g_ZD$Q(Y=2T4Y$-m^-cqY!wV6ugso@8_*8*4AYa z)rr?AE>@bF{9LwFjbGA(Qx&Z;h2tE7!==Q=;fkeV2KA$Bb*76OrV`f{EIEUe(1eWR z8ina2(3q#=BY4sv`E!gon^b^}AhTboU$;5sI2!k0w|zo=ed4H{i%D;mG`Ht+@lYqK zEu@)%($Zde+;#cJm2!t{2Gdv`CiVOTgXy(5(hl`7g?O3CM6UExQ;9j8Tvj!?^9_yi z!$!irJ?M7y1Db0x(w6>Kr})}$wg&0J-A?%o5*{FGIFw;=sc8|dQ|uoDL7jXNK+W&- z+A(eKDh#*0CmdxFrO9i`AT5nFe;PLKp3bLwHSG1{%dYlgJu}AbZH!pljL z5D5>sy|59>R7${;c$?v*>ZIBcb!+^J?Bg|xTwBXDZEUvxU#<@g+6`is6KJTDPsT9sk5(KXoD2_nJX25I$hjJw zYlW~-#rbU8?!BcA%vRoty-V&U-ddDo7jV=ZnY|H+Ml98T_yT3`xZlLpL?5G@RYjSM zu)}|Iae=fJ2b$!zKRB%Nel^%tQ!^>y+z@~Ewg{!u@x*vXqp(q$dX!i&)(3q<=)`C*t4BZ ziZ(*_Kpgh=_%D|WYtmu+>Ey(&%abO$)z!E@br}pdhrh(vMrLjJL*yubxg6Tmopz)J z%Y9%pcpX?sQ~EWQ)bX3f>CvD-{+|H^eG(UeKSh|?vhm;gw=siQre~BihbC`?J@oTA z&R?!+#WkKzEe|CRqxcuz^b%bcW=e?6rtoGM6uVA!2xGR5?>km8bvII3E_$^8)a}sm zZl-gNxY~z?%Wy~j=nwA{NWbp1#C|`)m2bRePFi>dPDo0&e>*l?lD=MHsHJ>8^e~@S zk!Gqkbj9OVMzlci0(L7_P7zM60ZDg`aALd4D@i3Okze}?_8Wh@N~Ft>j+M(;012p+ zT^n`Jv7r*lkhk1N!{JR57c#X3Tz#d=ioD>Oy4eu02+QYfBKzI|tt{424krxw|5+c1zr5tk0_9$#8w$Sq{^UFkN7am_~Yo-81#Md84fy_oj3uGj_mwnb%MV`6|t|q9kr$Tcu-oGbCu!RQ>?@lsI2$?UFPE8C#qa z&F5C<@kM$)QT&w~4(`qI%U@UX?#pjQ1P>Fg=7Tk3=6E96T07J{+)O%lieikH!tI;( zU%XrX?MmY$^9`3B@G?8E$4_w^HKO!d@O*8*Nsw~U7%*WuL0R(p;rEIlqAZi=D5Y4U zx2E^$7apimzuv-vj6FeanG(R;?JxK?J~vKF;QW5O;ACxv;bE&*rg+Ol}xSL^?5V^1vW7-gE)4iqmb; zRkH-Ly*Z98W=(`EY?KgOriwhE-nNdkzH}+QskEeMuo(?54rfep&BqUB+IWjp_Nq2i zushycP5CLI27_F;>hM#s%j`4H?k_}+Ej7-~I&#H>+sEf(n@(fmxlwXQyhV^HOn>SY zn--YPboz0I%_yryCM4)|@;*awe4c`cggsU6&U-^tsl-Bg9Ym`!2V)Uw)QLT=#IC~8 zi9kuGQMZ95BneFV{)0psd3)}6o}U;55eqG~dx9kfs8cb4oHy-YPhnQmDFMw!vdh;v zV>*uqiZxJ7px*>7BvF5}g)UX-6yb+KXb$5$>@Kq(&I`8gF;w=>s^!QWQos!cKIG?F zRiDbS@AWal3ZPEvpK9-mfJUzMpYAN?$8ud&a|b?V)qFMi;W4YRZv7LRkEEo5gN1rT zG@jZ4g~P?3rP`$thC5mAGW6_Hif5!%7JD;SVC*RgJSbQuV*5k0{a~pHs=iMP672Z7 zqgk@AF)n*s%6%u-waM7v_H8Sl*RO+;cOl^~&;_sS;!IUugA9BOB3?OIhO>dNg)`8t zA#!$|b?s;NV{*eg4Xt6eB^^OLEOtLzN!A9~u_BSNg_`a!J|`8Je%^)I z4lm>=;b!iFD-ULQZn6V6SZ6~tg&2)9ZjnOzlemLI%2)M@QOt6|7DKqlznp@uXEw$x zmnP2xS!K1$Qqs{2*-K!d+s(XzX{iQLUs1*gVeTbP?lY0d{#Ep%sR(Fdj=I)zu$xBK zIlDS)RCZ`lG494UvDrl7C|pm0%u)Jvk(j+yjFFI$+NkABEeL-4X=|7V2@O~LR(~t< zMH5fH{7Fn;IA zQ8mykGj)RYcXL*4MKEWEP}2^9QDA2V_L;ou>OJAF5XRm6o(0wn#(4mG-zt9p!_u3z zg2ek-h-Tt0TtemsyTZ}zuo`HK5W z&;P8&ed=q&Zq$2MRy+)93U|9TM-?|3_dIqCe>&GNc@Dnro+>eT51$Ge!4IbEuoUU9 z6%gPEoKtn&*t-zOlnvLquBmYsolEn9GG0^qr&iRudvYmTS+X66{y_Wv_E>U#(A}C1 zW+PWy?Etmm$QvmBy*6xp^zfNn8FNQGHLIf~%{{S&kJz+rYLh^9>Qi3$IqTdnl(R$_ zZ3rZNk9yy3C=>s!DGJ-u6Y)-h5D?$-^AgpnXHqX?R%t0!smcnWE`tLgyDIt}+ef8g zFk*}mJ`~~b&G`?!373g%`SSV7${tOu+Bf+ErtlHM+Y2Se*Y>8&+*z%G5g{>d%_rWE zEj9=Q-QIIiqOFb55uzRSeh*5wZTn1&1K3%9ME^$Tu}hLu!|ofXJ0!q3?t~wf4@JJnPDwfpt>qtfkpcO(a37r+A(!PwcI&Mxp6T5i>PJuq|?kmCA0CH^Yw<3En>`<^O(|SucCFW zor`n3!=uciPBq;~2dQ@fmsh?C-PG34CdUyM-9NDD7>tK^8UHFc)llBBCIjn968)Z& z`Df3`(D)Oc-Z6eL#S!6TJy;^E!B`8oj_rsZ}gnew{6t3w}2!TM-+lIf4XBkgpQ+g@T z1ya58ugBWl8+GC9Fg-6K8!HXVoTGL#8>K(SXT82$&T=v*!ipcsZs5O{BU9mdD$Pf4 z_-4RY_A5KlZ~49S)swHawmVTXjjKk+Do7Gfi=MrjR^FOqm ze*p%mR6UlUlrraC?3sA^d_7O{ZC)ZaUovi4$9dJ{wmE8p%49b-_vW{b@g2jeV=>Y^ zKG_WQ4Bqp~OBnw)q&v?Vm9M)Ny*Mp_TF4$sD zL}1ueR^0%LrTfW&-!>=DHW{w(qDm>XqiC%7zxs$DF+ZpCvd2 zMBW@~j3xH1&)Mwl8!mOjBGH^tw!AO}!zWTg_zQOxE)|SJ9r&((>rnGkf3B7v{!5@; z&os&AQyJyh$G)%+#yXif7K#(*{7(3(>Sv$rj-zpcRWou(Kp-G%UnHP}ueDbZ|$WxbNI zD|w{rZnF5-M)(;bUd%5gj_TI11hV?fpkD4ohpo=(Mrm0Z=iTP7;J98(#7;mhN!Uf3 z`Lgggvmu>Nf4Q2=Kl;+zHa?cXH+X9JOy9$nYOEw`snVeb5s>p{>^@+r+6Ez?Ft_1_ zSya3asfG79H4wb_mC*Zj-x@sCThZv!-%iXRA^`9EM~Z53cWDQcL~~|EN*MQOk+0`z zCz@a6AaQu;=Z1t>POe61wB&HaJ~Jiw;i#P`$lR(FNa~FwuA)q(#(X;E?>uU}RICg3 z9Wz?YAKs2irI`CEH`#U8A#;xWM2#{??e?$%Y%Xz0Ba8^bPzm^;q1VK5)#2zc7guje zY~BZ(@#N~n+FIOXCKl%+>O%F)liy424=N&13-$CatAi_w=Ek0hIqT)%`rtcvh-ZwC z2-m>Iq#RqEn0~)?EGEZlhP#J3dQe!A@u_ww4zfBX>w-V6D%6Jgc8}*dK_Wo(xN3Jr zyClvF?UmDf`Ms5;S7#ghR-Wgs{BUVi9uhQw2IZ9AutYGPnaEzUMC)`sKLO~a4A4fbo}yb&a3Fb00;LLP$> z5pH|;OI0l@$>X;S36=PnK84rA@uUvypWoD$P>tbrDEJnsHxRW`@Rugp`eTnDcd-+3sKK}UpzEol$3bl~9B%KVb7VHw%JxPFx5 z$hRd~bIc-k8JDRmc66;bfjxzBn2y1VzJ1P{PPI2}(fewtchjbnN?bJY^Ui#^Cr@55 zid6T06=o*Gv|}B*n+@pg2>TMAZ6b!-JFaDW<*`i(p>d4P-`{kktvlBxMV=v1Xd+ld zBL8$rxh1B!P3zXpt23X7^=3z;6gE1#?$LPsvNhPQmPUf($A2klqx0UX*bZ;q*`x@~ zX9osfXFdId{q`2qUUu1YjOFyULqVk7gPGZ*hVHh|@5q+fxm^lh18N$vlDl+%zLY(m zcc_O@%vCQ~|1R2=dAv(wu*@6;A-3Nw5f&GHa-+dj)PGCy)t~8vy6aLuf zP3x}{t3td+r%tczWv{w<6|*jJ-4Lwc(+9qqa{Q$~K4n z_gpqZ(%`HdAO>H;e&BoX3-5vB0_nk7s!Fw~_byUcvcsbF+o#vnXEjnqk=nJd3V(cA zI>JU5r@ZPB!5_Pf@z@jD=2B{)V_u7_U(~Mr;);dd#IF~Lii%wDlptS%JIKWJDkBL$ zDNu=yGuh{kBcrd@Bz?*P#wBBS~fXagu>#x!#3wduG4Y?jb7^ zoVms4sKSILmv|Uor_Gv!aT@w=OCwlfsHmaE!xCpz=_9^UK!IuR)bv#3meh!M&0i=M zU0v6zHlptB5j0>e^PD?*EC+vHq-ndJ!@aY5;)%~xlAb?oS~R%w*0bWZqd5KU#jSHl zO8-$mz8MHq+33}FN1BAklCM_8BFJy^PHEcZUY?e$*9tB!d^uyNZT_vcGe+A+pB&4W zBSA2A{@0hMcDI!a2T0iZ_Q2*0qu9+-BHcWgH)!a zmlZws9(fS1mn}+{X02SRGg{_JCJE86IdaChuF&u4_}L5Xieqk(QEN+ z7phVZK{s4wH~F+&T{(<7+&i%mTUxla8d*P#iFj_JZ}U-GQJjmp5LZ41Wn=RZ&Rdg{ zW&`))4l?4G&9Kj&y29hB#Esn1$<#l7p-5y9xSj2oRYwa|mj3?xC9lM7j1x<`UJ7S? z{xewW+mOrp)0|ANH(lxb)qyE{XU=qlXHHwWf1o&J#6%meo@1I+o zDtc!(1!@%)X!+N77H8~dR9{(|l&H187yhn6msB&76Cq$@R&h;CB)1?+ZmBEs`f1J0 zm9=fsK1}zcWz<+;+CcV#+3vLo5m(h1i6k;ho3Juon@hD?HPo$Z^DZ`b#}fD08>LqE zbR!-zCG{0Xs546uGp?Uo40!zp>oyZCBMm z`+nA{txP-lUUiiUp*HQg$BG9~sLJpIR{MS`DP`IqWkXl(`ELhVtQ1r9tw@JQ&;OBu zbo_Rafqaj@dEzP%tP;!ei2X><&1&lCG_iD!#UuRnxn<9~?ouUIq~Ua9^w!c_yR!_e z0PF3dj{>_^n@1J)9}fLms(X&!^gE>GoKg;d?#wI>IK2k!0=u0;-<+>F*E=JQaR2Q_ z#Of?mJ?MU?*!IZjj_uPiyS*BE5!|`Kha}2$b*&PwGUv#ryGuqE2s+iaj7U zt_|r%?HeSaNc?5|2Y!D4g&qUN6Lph{rBVL()US^DwF8GNA#iy-{t) z4$4mvr>mpusx)rPEP;&-q{OH=18Uk{fz-QOGNGm7&t)oork;iiP7-c%G|PYFfBFmC zRVz92zHQ6HFKXD0`{S8xity>ixE@FyI+3-@)G`}bxgNgLAO`3oFAc=hrj3;N@S zyGdgQ*0!`cmC@`wS477%xZhNQm;jG@{SW#OhHx;{?a$d&a3?G@<+i$GfiUf9O+a z>z94!w&H$IOWrdu5j zTe(MMga&0V^Qu%fcTS%PLYD{nN%(t1U=^!0C(`<_x;qcN@-3RRqF!OSK4sQ_OuS|1 z&HbN+pu5e?XsA)!x7B2N(=BJB+bGp_;j3Ifhtx(a&lxqY6g9ODaXz0d)D7M59b~u8 zkj#04$e>&CA{u(2fj?Dq1o7A8RA%2ERc1gN=-jP&dg?ndRxMC&L)PEZ~lrHIB%zYid6 zwHowJ?0jV1t44`Tpyn!q&&;u=4`9rtWoK+VV~npHh_8h}V2KY0>%$icwL zNz_K?#Rh?OOrwRFgltnGNSqS@6~70(b1>5DHy8jdL)6Ub30GNp4a2Lig{4=wl3=uqYM$&g90J} zhLzM3Jn2ZnMXqePKz%eqY?;IZSgPnMrU;`0KFR|u16IX+w_W@S-|+=a|AT8&M@s^` zsRWFk8YhCrp7`+@w0UEv73(9|Qo-kBIYr+ed;7h}usIaK+Vk0lw`ZaBkbV}HnP?`G zWAEkt=4Kvlev=QYeBA@(65+jhz7ii~?AAOHky_c~E8AXLp2J@?D6RfG)g5ZI9cj?k z?T0#R&3_*muG|p86VSwMt2vb~Hk1{_stBR;yaJdK--=1VSl|9~%UN_7Tf%Gm8+gkC)K$bP`;~41h%;dR@c~j)`ZF|HTt-HW#K{cN3jiW4xgP|O ze1K7mo7!Okbq%L*OV3>2YJ9#PB=57&v!nE~2F-S@&cG!#lcRiGsj9wfhkSImGm{e| zW+zY=ikS8I6(|!2>L48{+9v9tobeyZAmi@HgEFaZbzqe(J?c7GBp6<>{F*7ny)&ft zaWjf=MUU)-olIafVm?>Kv!>;k+?#XGAX4Rvj7hX=O;_l%Jon@*=|hyEKmUN|C+d&= z+s_x?hwoUKux50|y(!#DG6~FQT2t*cbQ_r}vac6NPjmSE)NHr&Ff4YtQLk`hQo(d_ z^l+C#@tK%fVVhnantk^L&XG2YXt*QCKeU<|x(RN0C;ka|eltXV2Ti?avlkm6ir038 zs}8_#!KCREGP@73SN_qn6kn8j-Y{IXA3I5_{fYq#-Fx>Z*8c z7|~L#rD-B{XG$+_n0AjXjxBF`d!1FL?>Jv~ZJk`+Y~d;`svNq{M4uEtjGZI9Qdv1v zLpd?fQ(04=B#B7LR;liH_>HnYm%`ZdQ(ut}U#{$mCOq`Q>b#JdtFLVV-Eh5rpUwc? z@(7nUP~vn+E5`5O6?=eav`Gi%_c?FCf z17E77q||pO2q1W=0T&!#xySUn(#1qyl(o>jY-fO2WUyOwF&F@JZD|f8-rQ`XhX(3H zkVM$e`FWEmpM8SJJ#}#00(>A1K!m_vX%=eDK!4T>q9OWGz~xLp4sP9ppqt%-Q;{H! zPS2-0v;w$nfEmwK*o(njf@qOt7Sr0hwo4c>*fbzm%K(x!*6wLjoyYcTrVlqD!D^xS zT!^h~T>4V2oZ0?Ov?g6$u_mgfr`gR`uZ~?Lbz=UDKK(aaFG_XSgE9>EW~FT9cz5$q zw8nK=bYmr3XDw-FWL=^Ryz`d`zkEyeri(X5UxAnj2_LL{YJ%~)ByqmTuIOuDx4a*p z79YaU#3_joJF;oJjOjnvo;{c?q_`05>C96n-31h1#4{|ScE;rXrv-b}Se-BEjQlfS zDJ$f$8n|-;Hd;G?)q>NqRx{?e~tpV0en~! zz*=oPlfYR(0O-q)ZpT4iK}AK?S7oOkNh5r-_rZ3g1GWqxk{Efd@xOzkbO9|$#?X*< zd3hNMo)E_5MSoUMZvzcDMouoLiNGb<$fUpzv9_^+CSU^eCWij}09Pi8YK~gadWc2w zozM;J%lHp1ESSJ6nGqtqj8E|jV8Byl1A*=y^G&Mv3RH1Nqbhgca7Vun&NEDH@9u*KNr= zr$|XjVME-Ix}5=qN4&7Fvn>%m2yKK>l=}lh82AN`K{)X_O_da%1V$&#tpdvp8SWy->I)<-ujuQh!y&2_iN=713}7_ zd%cF{`Ntw=%e6fg=+R$HqDGc=5znndKbm{*)-LoM@z7z9?plEQ4C~JSqCR7D{L_)K z`G~7@+GML7v(c&$!%8h7<~oHd6%r$3Z=bnK8A*S|bh7$A*LslUb5!@rpf0W_?awQi z!2_-JQXSL#N^KigZ{E$9HeTUa(?`Tbvdea;lblF6`}TUKncK25M$!GB6=;^VuPGKO z=Q@@RWe5BzumWQZLzkDWBWXp+hKGkiM5ZkU<8dqUp8Q5B=E#vyrA^r9K77uOyZs2zjt5$9dKP-e*K^XoNZVv_;UfbdervA9p8%+PX3Wl zpq!jVcA>3vypMgVxqqjOAmuVs?L;BGr>4uoO+x_Lk*jU$GG9w&GMjubR^Q``9!rSZ zV~zl2Bah9fN-e~RJNI3gRJ!{Z>Qng-YAu7XDE)GElH7 z_Q_kmaZMw2Uw(XvqGsyAb?APk?05v8=7}pRGre|)ediu+x`W*U+SD3TSPD--iB$D5 zuG)skTqYwX&Fz%Zn=q-7@dw0n$3{X);XfKbAZ_1X+yRlxAznE+4MqUH#Boqb5J&0h z2sn;zTnj33-!%CFl)6rtS=f>nh@=~{5uCT_VzL2M0?^Sf!xG-Uz0J&wTUl9|1lZvq z|AOt#4J2`!a`~NkgOkQ(eUer+l@ml<3D{Q0tmlJ~aa6C*zH; zu&^C}#>vit5Qp!YBQ2hl#oai=r7V7E2Jz2mtF7*OmW3bLISw$1gF-}_?CTiGtTZ>zaRAb>9GrOxwxVvIa! z4Qj8b);(%f`I;B@5yLuXl-gJ9Y3og@aKBGa@x-O(v{W6FI?6&#IhDlX=NpwP+sk;} z^6Xu5(#a1Y+x(5_kZU|Y6V!wpr^8Trd-*4+V_j%^g{x{ugI$98Pr8Q>P-v4+3*&v!uc?hfmJ7$1Drl<&l& z6>gL{-aPR7laabNQUEZx5^8o0>h@H7avk#jKBm%=Ax<|?9<*8gEIlGCY*QZzmLLHO zm_Q&9CRVg1pofN%7BX!>Si2t45}TSD0jU#mLzlRY4`9V`c zDQPBH2W#XiK>EIRAjTb<5AoR>#f6wKJZ7N?6?J`mYTL$1ul1-GeBt0%%(jpldpWs8Z?=Dc2fv>w0!@a$9%FQ4Q|8n#Ci!pl|Q`ev9~@&Z~4wi#_@my>LnXcWW5q% zcezIUka&dDXtt&^0A1N5shzyWgg2>RT#ZeOnV28IEgP>FzhUh98|3{V!w`Qv2IFAr z>c7^GG`z)FiKjX@TF^$cU^oSJzfGV=W%#8wr;Utirstg>Hq$Y%yX@m8?Hg)~eP76* zHBs9cnOkJLM2zLBT0Pw!LY%Jqr-X#h=#$T=EA=mLzPm=y>VN%T`(Q|F7ytEJ*UIDHj~L&D5A;ak3k2=Um6%mZz(sMQ?f(fPvG$dGiL6vS3$_6&*dD z|BQ)lTKC--g>X{k7Tt|+w2i{R*__e+;Ys`kP}G-Tdq1329&CKYhPd<4aQH}SK|%n@ z-XE=Yn4$x`#r#j7nj3@ge0)@?xd5{_?^$_FSs4$6@BO@1HO}R+Wd^;0LOXp@0TwUD ztJq=Bh6^-75V0?z2AQ&qw6t_dGRPPXTatvFZUcoA0+yqhx%mu8$yy>Qc}oE7>*NUC z(8`?QZDI=m_c0*XJ8E@c-|D9>)xqCh=-uDN{tA|pgK`d745niQp|A~f!x<@re!3a< zxCC$0ou*WWZKUqW>zMt|fVvyNWHkyt84D6_u!JGGuOe~E2b)g!`t?oX{7@yEQ9^tK1+gBb*EoQQQeww6;;N5>iHu|sjhN5@x*T-skc#6JzSl%0`JLc$fYjIjjRc zF$Qg|a6D=2(|4;^-m#@$-0XwWHZ!z2$Z3*z%&-04ac2+gawYL|nq`-8M8g>sc-rc@ zir)b0FN(KE2ph0Xf$-+a2z#O|wyOd7uwpu;CO9CC0s`1|+)fZePl7b8ZD(n)+xdIb zOvw`ptH|jka#iC{Nbo|Wbih!Lv`qK73?T-B@hM;Lt&QV$W0B%Tz0-rrHGI-P=Nnd6 zZNZszcobm!D=-IDq>r2G4H1<?vbrsohF7{HbZQ(!QV-~=_Ct~o3Y<`LZt`)$5ABj%%e z-mE_^-B%Ek9f585p!GU#BewqL;?z~XRx!a#0D4) z`~a{*SV(wMX{!QDP1|MxJQ9+;YMPosU{8{9*6-Z3Z#Z?OacaKdetx7K_weZiIAO{XG$cxQQK1v1dzIki$I2|F`^>nf`1>H*;IxP~xuz9^=|)~U*w zuQpk{EhKT*Mt7ntItuloHoBTA)*CIlys%ewCK!DVXUE;s>4)>$au-$UH1UTs>qkBK zsEk_%u2qmx8aMa*WiFaLSoT;1aEM4tCB+9H`&s%oTf7y$HT{lTWRtdjxHp?lI+wl1 z7oNMlvnJ8r`iIBkRV0$0Cq=d&70 z>|9UfNcf7Vsl)DpNfM|hER)6)b#*NJ4`lC-=~+)i+FLE?O&BR=YIpbNjV}tmjx3OVS>S(#;pH9fbCr_@*#L%U=GB)9m8@$TOx(x~c zreFf8YWs-`SqSuMD{xPwU&~{lj3LqBNOn`~b$j#b{jOh6S#?{{(otQ-~= zmaq1;Yh*NlvU-vb&jj&SL9%g+0g`TBwABEDL&6>c z)YVfu!Amf9y^3iWJg|1o)qxY2DqsgC8{pJ{Dj2z(5)xC@Gfrz`l?P|X>)k6Ziw)YD z0z}{xLhtOZpozdY!-g#9@W|M>d z89sEx(_c$gbWfe&xH;0&&|lXKf8$ha^QQ9GVx(&-nL|X61-V+}pM_Wr8pGpRn;4Nn z<2K0CnPP?2=#tD$gsaL-DrAc|>b=s+w2d?s!{X_n<=gl8+mVSMYl?vTZ?t+HD6zlJ9EcPMQH$=}yt-O7ZV!P@5q{vUnP=G$cAlRG zneYDY!oq^-^jCi*zIHw_qk#R7P#iq6Bw3Fake3UXj^EIGAlZgA1*m+q0E1<11^bop z`4O5)N(+P^`k!-i44}LORthU+IarAbUSq9bpGN|bTLH3M@DV&$OdzrH^yU;;&}Vtx z;G`HSd5#SO%j|h`=naTv3uQf!ZF73G%UcDhB%n*07d6C>S9w3_R}-qnR0^$C3_1ew zV+y6WF7f?fxBBvXJftQuTB$ksk45+sC&j-7Xbs6*c10d*6~F zhU}W;8*rMR!IA>{hIxf6e;YQ|A39a{;q60iY2Ef&zm+Y{aq%|~@gR>ToGpDQWBgY= zd3DAMNIaKNgEvAEPW7efQK}B}Hsz;lFj5uKxAmPGUn+fGe!gMilcbXllGT=dTP6uX z!i>{>C4N-zE2)^az8UgElQEl2ehT9@-j(^?oOcPgWGFIPs|Q$6I^&}h;*YX^t6a31 z$nja(js2%OHljR|-TXs8W{`cnxkg?i)j_H9qe=ui56;+qejf=(AtHTO*w%XUTQv#c zw5!_g+(Gu)#=G}%atYrl&OCOHc=%TWbiq>t81O{z>p~sRfw6nx#;%sO(Fv`5Dp#EP z>W{s-^_?@%_U$W?-A1^f%kt1wD9Qm3LTTzM^Sf6U2F+Ve<7i4=i)oOZw2H3R1w6j( z&lvOIW{mhzeg_LJ$fk;sN7lgNrJ{#Hc{DKc7yvI6*gRV;6u=LH7@|40g#KfTP(|}L z_!N|{g0)MOAH_iQU;_q{-=7I^$jf3+8fx;PNDNV_|0_C8g zNH35*ceyb_%LQ1?1Tc`ur}P?w!!F%po)SP_X+e=8_Q}8rII9Z+H87b%ot;8oAHlOod%n&r{|fO9UGTxY9Gh)b_x% z%$1w3knX7kF>>LZGa(NzuTv9&Ak3P;#>NIK#}w=c8Nl%2hTS5Im*uj4 zmP-1a>{;gr+|rk$lKhjsvFb828>YQ{47T2P<;NZew(9)Rn*@p^TG_E7h4~|w4kPHIfHJg)Y1OeT&iu8h< zFs;eNcZ)yIUaJE&*HD>kh~8cbA`etvff&+bRdx|6DO4F%fZl}#n5?Xz-h%Mmy${JR zu!tTiDG@+w%w=`thVy*aBTz>HjwPY^!86`GYWvSv5oGL=&!01Zy%PW|&rb$lZUS@Q z1`s;^eX(c#m7>2#%3ZNjC{s(ICU6BeF-EU`o=>sa0INHd;QGZM4T!MQ;PqZ}&6$JB z8Q^H7+N}mb_=jT?0V}g8t@p+^5XiX@iJZp2R)~0?ksy=c*5{bjcitDWYNS3REDxhF!GsLYg!AcDoRNq< z&E8wH+B69rkqI!Qa9$g{-CE$QkGmUMNh)&k-|x=QLa>Ve*_+|X;ajh)we0-tuLi|f z!?;aM8wNNkpmu-7b*5&exCuOgz#u&G+sTTz1qI2IlaqBmJ2L$Gj3J}}o)>6|xC^#w z>6GtrvJ=Rpm>`w??AvP`@Oaz5f0t5LCWO`{Q0COk?;J1Dd98Ar8iO?kX{P-j&O}IN6Bo0!)xZPzCEyj>N!!-Oq};$OC41-PELe7 zJ@Zg-vBF5?fU#h5+m?H)qrh3Z0u?bn>6A8lzcm6PD@NjsqA7%^4Y-IjG$HsD5d*FOf<6EFm+p)?fK ziHscEIU50eGsLF&%ViM4c~b8I+lY)!=M`9cgi^uyPeva|)-`La=Ven4TJ4{fF4n5N z?haf{*a0<^kjT)}DRe*lt19RaUH98^x%vw^hPFQyahz_{^b-zs|5h4xz`HrvdSi1M z7Oh1Uy8anu$LK_5qe3mAL?g<1D#A4xNidZ9Wv?zrU*cGVk&!t2?7$6QdN3q zKtW|Q^n2Dx1p#TL3-$iNqmN|$ZfX`&ncX|!@*Rqvs+8CFgRbt1I2&8V`52LgA~uRL?A04axw6`t_wf};oN69&`ASLA<|+KFDp-NVnFNG&H?9TzBvuQu z#ip2=pxc$c4*>k)FA>`v=04ynFncMK*+6Li>PNyx2F*sJumkxkadKDs<=SfrL+bWo zwRc8hIij`e+CFx+K~A|LKRZb3GYnIV91MX z^v70#{i_v0tS^fe$EIqbTG^Mv!FLHyWYtS>m>|_*Ecze0`1&+P>zVJz-9gvPPobn2 z9W@tEb>OjXi3uZ0(<6p1tR?=ycjf%r2WpH*49S}wjXKDdB1o(gQjSxDM|lyuT#fmSh9fc=GJyTwH)xiOk~oo5(x zr7qHEr0eCIOAab`HcE823bYt2hz#@u$dXFIUpTnZ zMP0P~U0e#SbE=|<@uU+;kL1MY5)-Rf^U1q-bJG(~Encq@$y%8Ws7`N6SZHH4_SLJf z4Z$eW1?{D|mTyM+s=SF_ID9MJ`_H+0IUP^*+R)6dgJcAS4XU*`4)uT8`3stZt#kTu zt`mIg`1w!WuU2Dse&-NB^JSkr_9UG=dwEx@7=f1pL&m7r^*;K&=8j!I_5Il=^^~Uf z1Y}0&nb3WO5e#vI!Uzao+o2%_>wB&8QRo-)gI~f{y zv_{DftZ2@7cRl8MdyP&BoBHBqM-N1Tbp;U>>wUy|Y0d=phsO<@KNTW2mc97hck8Jh z><--ivA2(QIvPd4mugFQ4cwd?wup+<>g_3Psrz~RiKRv0BKnPv!#Nd>VQ2;=f9oLA zY64w-l@_9}$P{Me_CIa7&EPVMs__R7^-Sw2=%w;zBJK9yI@USF-R36&=( zTQ@JTFH}Ngql%Bd{PS@Um~!;_iN;~hfiN}CY?N)gTs3P`HG`BN$p--nLg6 z%(S84F9i$>$5Hdd%z}b@BTDULGoc1(3x&Li{kE)i@v8#FWrO_;RMNd&_zvpj*#d&! zMaNr|3FYA@tOKpaUhCDC>m9q} z5ogox8TG^QSuHpQvXm+D`j-UsKZ!=Azlu&cdsehs8V0YZTo@U2ICk+Hb8qZHS?#dU z>gVYf#SGBu*W3uxleiJ*KoJcAb+NFc&0EdR7>*`EVQV;0VeNXX)4}o3hm$W-0MB#mMNFn%qp3DL2 zU6Xwmjhk_DQF~7ASTUYIQ?`fD(!z;S~ukFvz zd|7FcyFJtYs0o3aMFiS*8hy-WA4Km;WxS00l@Q79Ya8b7!uDRA^JxYzzprHQc|v46 zlXAzjDFj&MgEzwLuL%eUhU>k}eAzwljF&-_b}d`&XW$U+hvL`F4_-B0D5}zHbNDJXGM=j9(X&@Fl@2_ISEm#}4A5KjR={HF9I7w1$ z6k@K&v~6^E5n(yK{A~!oUaw)#*Lk=xtX#ZVXYk@$_-k0zfX#HcU-$&f{ND? zd`7e4`$Ybo(z0AObCC~*S@M=SZ2(epP1K`FkdmIsNRJlisdA#7ws^T>g!bFO)jGZw zua4aO3+#CyAJJczYo4?Ww>;=+e)|D2qD1Y=7=pHJy&^#+dzvd~CS*b!OS+q7FlvtYC0GnVYg*I)a0!-TmmYEUM z3YdpehMtdH5)wYa=TN~nIaX;4IvvS3yEx%%E!u>U*$=&GRRqCh_VH3Cr1s8_6TzWL z3Wv~e1he_9J|*jBCDGkm=BHh^i0^hEg&l~bhkwLy952aMtPf$Yb=Hg$N{95+$A${N zS2`~Kys_1QQ(K)FV~;-KT&zy;1wwEUl|^q^eG2}l)2%O4@>Ti%a})AKmCh+(g*H)VRkzl$r$76sjNm)_lr zWBS(iHmP@I$I|%TmyXfj+ulANGF2F2RNP@y5ZE2LSxYHp*Zv}lzMOPUrvIZRhaRap zPAw4fi4gC_ipeZkvfWYgw#^UJE9CaJ{fWk<8ap!Ss0zyEd&GtWRg{~^iR$zsa^)6M zkpy*OpeI|le_V|kGky5}zic(09ryTzIh_4Y$ge^y)uI^1X&V6SXas0Eo&v=%p}8p) zSJz1av9WA!$xN=EqupJtY#q~6Umf!@_2LkpxSF@iZ_47=i{Olt4ge&gOujVO{8ao6 z<9+QV?v^WRN@JP6elF0Q6|K@CiYdpO_T@L*#0E1Dj8jM%}!U!#&L(p@g^(WrI(lE3^z$>HL8f#=FfIbOxkgcTc4`6y=Z5P4Y5-XT*U3cFNeXNK9(^zm5}aC|li~ZyOngG z>2 z(ZlyafxxG7nTG3Lzu=+g?3+)BCC&3#Um9)cxh+w9*VFY$=!y%D^#mQr`fttbe?sfF zzU3KM-2Cg-TDe3-4cP>fA5hWc{PNNTlreu<1hnp9JGcNivU4BE4 z&-}Aw2|Q8ZrpGGiEg!V&+^}osO693jqR=m6FqKm3@+PEkmAoQKC`qWQNlerUG@l`v zuau-5FGF_!s+TcUO}lt?H@JN&AZ3`Bao;qpoTGdcKHtEJ?8^*J`hPjo$W`AWrDLuC zx{t7ZzqP^sD3RQlAR*cXtfLy#?y-YPvkhmYcB!whh}&8bO07$`t3OYd&%<;T?~jXY zq5G&V-J)6}DZ>b;aZqc$d?7{%FO&*{>q*GA?U zn6}o}{?#O_QNu&7P+t+v!qt#KcXlcl{VP+D=T5KWSSP+F;?%CQ$qd}Rb*d;HUa@E@m+ zWP49z%y+?AiE=H>3Jz(~*#wn$dH&7*7gsW`OAy})ynbP?gvhmL@E1Fe*WZ6rmp*^i z`kjm16fRIBLizQ3Iy%6Ib?4sq37O3{^4udq^@{6$u5~>l)w&xZ$d}J(aAs54JcrrB zx9rmD!TOZ^!t$7&Dj_fLaep$rxB5hq}G2a|=3pSW9I7x*=xU(Eoqh?KQ;lBgCWd z@46UB&wJ!GmGBi%L(i1V#Y{@?(7-5f;4++g++he@Z4_!a5PW%#EwYA`AoJOOQj#2- z7LL@_eR1Pz>bVtd8%lg_O2rGN7siyM2<58F)Y?C~&*vHQL(lS>Ni9TX)(&6E#@X^mezEjV46})>{ z#>4YtI0kLJJIj0kYrj%&{2#8p&-}}^_a`3>^WkdPn#N^ibft%HI>Ter;{Q{Y2%G+l zQTbLfV`(B~j?;Q}+M9Ha`SJcLY+8`q&>stzE`BqfVLQlu=fnf&F_}2e{5l5l4>lXdSkGsLkFJz7^xTa zoSRQca$`3Lz54wv1H&1;Gwj_UQp|pFl2$tI3KkuJI-_CHrM~0u-6rE z(Z@vE>&(n&9fRf1WMdSj6bA0{AIe1v>^8PmgAoBaj-M8~(`?8EoXnWywuWa_3oPb<8@|85ELnhOQ)J@B8I^fP`Q~3Q`(HFRRRv_bY=Bh% z&)_O5j1wd+v@}#68y82pI>uJKl2w&^nSYpC>0@D`w5u!O(ar>}wxJ(&w2)_h@7df! zMk}D$_vPAoy>nUCopmNBYIAbj^T_Tib=K}m<)w~i5}#RN25;=|?v2uHagT*)B>A!= zEbo{tJabPKaIZI(SWQojSRy5LcbgDNurQmZuU(HmUzrp;u=3WcS@<~}0GgzYj`v0= zVobL{Gi0R`W9RnP210DVnR0FHa24@ujI?)WErL#1ILS!OUl8@_k$XshyN)zs0j{IBa2!k&PNMt;3sBS`&a{Zw^Tud{ChV{cR`t z;+ew;DGv>9jgUQyh5i?R^hTR`-)N(@jM&Uy$i#GjNqGF+n;&GdAGeWja|2obypU1e zm?=6vFTLDyrLjfO*!#VSy`P2ervES^*8(MlNG{OK8z?T$iK4l%IE~1As~4u6nUCg9 zy2nN;xx~GZIrd=o-58FR>FS+GIu-qtODj4%T3CFfPS?7iaI;$CoSuUkUd5VdF zI4ak5G=JaB%xtzfUiKNnXO9;uWdtoSi-*M}C=GEfn3eEICE98g0vT?6R9Wc9juJ-&0C zQ*n9!8zOMof+qhDS{j;`=KpN9s;<}e5nx=MYbtN9DIAl{WWtX>N#7z? zTpX6xu#WZNREO)^dD2$8-eCHG{r8vyQD)K=G!~3dVp(Wsr0Bs&y^@C3r#9^K&&1pF zT{2o)0?=o+g>hisyDfR{3q|Pb*Mgi*p5$fQRHuVCp!N-BB$JteEN14=GNbwT46hX@9XW%I%twwcn zd2MWg72B8=5^xd>BS?}&9C?B^q4wQBOLDF+=ev`mzBpSjHoU*yq%4e`tgHJd-YyGS zOz62b8UQ z;I0?xCvJT_BBvI3&qLRCCN14g7=m5@wkkPUu7H4|aPWZ<<3W2AS_>gly+74PztFW= ziT&`)*sapXX%FAPuJcY=eaO3Y^V_H%ICzhGrbC{aYC@KTLYQtw?=PDwnu-3 z`v-rH_)SnF*7#LkB6}0<_5voQ*pPZcwN%V1J~?dp@g+QK>(_=l%)auu*$IC z$k3=PB@3qTEVYMXDR)F^TG`+QEwNffeRoBv-PmSA|~4Jwnz7Gy1Z3s+s8*pKU+mJ@g8A{-3dF&d7A{7P}$@CP|RY>3A+W1OvOQo!)S z<2rBqlC-zifCa-QqOY5v+gB^~r3G!9uN;LUPfmswN_x+oIE*s3Zo=NfQ(g1NRKuW< zd!*!j6_^_53ZOfjZN|r!A}sK_#TMyk^bcXbvvV^kX(ORXElSF77E+oIqQQB`68Sim zbnFPxOAAYMT@Ez*^5x)j)+J-xk?VYq9^H9ub-WgG7(y3*U&O8T$FemPjoAA`?{Nj zeDuU9rbAAfK)mkHSUKDQGuo@8TUE&1HwtGtJ80B6w+JVj?TtoL9t+?Zwf*7G$4etp zq}3+Zbcm>D)o6MPkfYinaG#asSVYZ4K@8!V#Bj%ZA3UuFkpQ2Q3G z5oKitGB-Zwwer}_e-PnefGrVSA$b2u@df$irF>&g(e%|)y4~wHxOwSvv;)7@9N~HV z-rD>sdi0)~@tko-V(@x)wvBY9BU^;@W~BecD8dWVxlgL|-FvR&2pn4r*X~U`KNG94 z8vgNz9RnxgE7KlVM~dy&>Jc@Q(?onYp%V!RSjk<|AL$nT@gj5YM z%$jM(NO8FT1N{FV59knPT>KCT(7)Zy@uetfjEym3r>UCSu!(dTPo@~0v7^CW=lM@U zuZ2Epw-Wt6eyET3!LgjQPvh`GOu_o+rNIGH%L#1;CAE#iY^wepEvtjABf#330nX(mYEPV3(bu4_^+|olf8(!m;vjemu z`4uT>#+`}5bbZ3o7$}W_!P&&T+7(@5_@MfMVk z$@G%`Nb8G}!)m&txx8KRk@j=l@q^`z)u~J~ia(sNNNh6PK0Fb5#Kvy<;`kW7Ngr3U z)G<7-+lD&PV}i|oWsF4r?G12Vw9vUmbGYhsC4yYI-x$iS3NMf>^PMFB$`!^`F<&Ob zqYXPgwO4RNuF@C3<&ZIA<28=Qk-h=L3owlJ^a01R7@xVglqn+9*gHQFEw)8UZa624 zmQ|^;9xJQ*&ccB%-G}PuJNstPo%J^LJF-9B*-NBed6PiJ&B>2qU+O`_8EkG8JMrra zyL8Fo$A$0LuWjrBIx({JWB;hZe<3D0@^t3a#V;Ozx89-Q`Gi$k{%P@#b(Iyov*zjL z?ZOBhr>c%|>GFyLN$a;*GNicC`DF0_V{v1_Wl6xTgr%l;y_j7`n5DpSw&S0iy z?LCtRUmugbCduCXVf=lgsgZB0p>Ve;$)KxIDfCxXk3?SopX75WSn;Edjy#pXKu&n> z>Yw_DR^;N_Ej}}Y!G`^fgEtkxc(P35aT7@9{$J3phXXUa!e?&Ey^-txLSerwZ;WJj z&sGjk#i>Rg(#CY zQ5m}xPh`>9YVHc)3g}jM{X=E>{hs^j-i}@k9aAo-J}5}XET6vhwNDr?jfqmqh|SNT zfIN#hLF{~$hXyIjdL>6dk*PoaKwfK4w^(cH$jJL)mPeyK-u}hK*aCPA2}5y35AUD_ z6r1?AzGZLza_DO4F9BY3!$O@ze@V#S|C%GH?T{;YvPbN@rVsc1rv(y7!)$w{$eykY z6(C3dTOw#ekrQ6e0bG&n{o6hG2WROMejc(Ht)N1TqINpIO3r(H-~D)UZ?mn;&E4Y} zjo_@)!*Op|_vS;&!KpJu$vPQbI-5t?R+y+Uk6+=MMbqug2w*`aCG4+}(nbRP*Vpqd{sSnq#Hrd~8y@F)N!2u0))xOLI zOG~r``SlPeF_2Dvx$h~#ghfJT@Vn*+Gr^;(#FeKjUf}jnl>+tP7iQW?JEf{3TV~Ir z)zTV{HkmiCr>&}8r{DQswAsQJ3woypv7T`0;FF<@_bB2w9B^LwjCLnH(a1)-`yGFV zj?mb4BfhhJJCUw#Wyk&FN?ozJfJXkct!~cJiV0&f;>TtK_(ey3Tj5`}XkX-k%j2995{Ulnu1?sS@hH(B88gXy1m(LMde)ty$ z3K)-jZ;p!ytg3doQ13I#n1-q!jTBi#%6mE8bY0Pi2v?^h!(ZFR>%wH-Syoy0fDkAJL8H$v1A#+Wggr=w6IjvPYyZn+OMg#Hu5$?I@W@go1dQXP< zsQ8Yr@ilw8eb+ormJeMam=G<-u9iQM5sf^{B7Iw1_^i7X$^>7wdehI&FYs=I4q9i{ z@cw*38bRJ8GVZ(UTTkw)3^qP6AK9)&(O{j>)ws-A_vh|>>F~s%3_Y_8o_qDa`@o7m z;@2sS!<%{bgHPzY-k<_0zO{p-Dc))@Zxnb?m~ z2-b9puuG3Od=F-h0Y|z&;%24RJj-v}a_PpW?8X#kX6JM(UFZe?Y>Cj!kg@soq*o-8 zu7cgE-Cgp(Jz65=yR0w$j5%w@<`UaA2}qs z!?%*VW*Qx}CL&C?M-xS7ZpnLTE&2r=`}3$TQRpb?)z{&C0c7V9o#hpVq6LrC@~&%Z zVaK~C?k5SU%P-wJq{HTAZ6ecp&c2pkrMilGzv@Yry62Jy?=bldVm9BJRL#7Zu2lIGId1c(n3u3R?pJeY8E8%yLzc9>q z#f5it+(3*S+f54+1n8%USgr_a)?v~i>y^cxCQ!<}qL2XK{L`k6xY&oPzUM+htTp9u z2DrJW0Gi*j`0zaJ1v4`s1BAA~LYwwHDK4&L&aV*#=^_yj>9!*jz3_|Mt+r1?Em&I zYUDdT-8x|yUzt263*Jph#eTU>#-pA#MwKhP@v=T!N_#<3@v))bSs%`Ho}bvd&8%Ii zs9lL@HVe-g1!?YFn=86zWpCg&c7JpHcE-~^!tV6Mg)4ko=HH}o6)I$0K8$wIkG`oo zBeNIVMDt6XuuHl=UmN-;lFqmVD=KmiyXE?t&1Ki5Q*Mdg#LHq|@Ws=XKMraTC3;oJ zgfSWL?0e2oXo{`h{GL8M;xID8GaU8rUb41b<{Vx%Scn0!PFgTU=J?Ri#6-7+9DPTk z#oUfZr53CT$eLH`X5X~GZ(7w?3>N~ybTj`C)^EY{qVhI)2`>@{$=hm51 zqP!agjm$R<1mUmNw%zx)4m;1|vYob?8cq~{Ej^fOs2O(N`M{u?XdfvlR~m(Gdb&ru zCPK zd2K1!Tls?vi#6ewWw#c>R!BwiCx3*hDh9Ths7IVW>_YzTz8W6&J&Rf-P|T7S7bOnX zvt*x>M&R>Tm60REE!UH((px8Q9=O}zjj-CflWE-k{P6IlQhN_j~o=W)GK;-;ZLsu#;34ehgH8E_<-YJI%mNU6;sO#C6a{er^_3 za;A9iCCY>^pwe1oJW+gMoqbnPYpB+u-~GUAlJMYp9F2rRNAo6!#+9vH^`Y5A=gi$NKTt2kT6NNW@b3Q9jo+xW=Q+^n=?n&bhqw;)xF_V~< zYhBCV5kIFshsS14lI9o)kw4ytouD|;IT~xT*ncgV21WgtR0YfpkJLFW#<7NPx?(7xp*G_kG*8=o=N(E@_GTU$m~Lcq z@|f^Z8^s|7if9b76ilivZ_N8(BHJYL&BUimN?lpEBa358MPG2zsh>ktTn^~X3qP9+ zux|^e+ZtfqXh=MKFQ-Kt|7dwsIYl7H0COqN+@GWCCTGHByye)Zyxhkpw>jR7y-eY| z+M%5+K77vXrOV(oVeUdwvfa*>FTEeHvMS5233KE7RvPZNED>{-I3$%gAkO$w%jsV8}X4!kL{PPhC zrQvzNv85S0QoNP#JaQA|oio$bX0>k7GjthVttooqDITtSd$}2ITcg8y$Q(QBvV+K_ zT1r*lYL0^r^9O7zf<5NXHFtZf#r5t6OngwTbfgunE@Tv~pvSsyBpk+j!cliRctcL$ zg+$-a1+lcUR+XB9L&-1;oeT>dq;z8Vkco6pF*7LOZjj#r4-<@hqgeAlmDJJjDMkTL zcKps9DHw{5`H)Uq7~*_vnwNUkzwlTWT!MP}Nw z>z~D1;7n8?NkjpWG@&Pn9JWc#hSIISCa6t=(@cSSwO1(MEU->Jf(8&~J=j*pL z?k@x+zG}5KUp3$iaijKUmpL1*I<@)z6W$6w>1|(zQuBh~9BuMVWT$w#AZ?)#;S*v- z-AdlOgKhH8`nJ?Y#sK^rXG3Im29A?26kBrWt<{vUC)9fiKVvC}u zsoo(MP{HEiBn#y=KS`4=!EkYvw4QAk8DY}+!GvZr<2SLPOl)U!97=MPN;%}LfYOTn z`K^(Mun}7g%r<*E`dfo!2L@OQx`~&=lAXxhA~*S6O7Fw&T;FhDfc~st(rTqkydPhJ zkN8-vgvV<|a>$L9sep-fJV{x!=!J%QZ#j<6L|N6!EhmI(yRqa4bKb6ryL6rO;IM@3 z_jb@!i@$THAh-EroFHP>?N0>oM5h$RD0<2r*ztzb zNbn}Jad+1=!W2K-BJG{X3?FwRYZ?rJk?ITfwB6sl!fLHL9eG0)9E)PwgsndJtimP; zAnbSSM0|vY%-k&aV8k9-J#?eG+_jkWOtEf3g}Mc|_V-#0nHtN+a7aHfO$pXIyJg*y zmQ`a@X7x(T1s%NQg~eXVA`iI6`k_l}$hkVI#_Co(cNG|vya9L2djFUN?Y-R*|^FWcl4EyG=U*9R(gBEK%cU zI{u4JYFA$zRDbR!t#2xy&B_8S3jzs8p6J|A%?5u6rzP)V+uc16{R>dwQ%LklWOFUy z5P#x`L661O-YvPV=>J^hB?UM0pULL>aPdbIT$YK+w^SVR1^hR4 zf9P9S5dHBN)JmnEmjlP_Q#!jB@_WKLdxww@&X6uIL3)Ux`Km<8+LV$4s@s`5Ia zkNPsXu&h?D6^TsgZv{mb7e2vrJgTYU_~Ua$X%345o?BksEq7N8;{g-PTR14Vv>xp`Jcppg z{vvTs@*Ziv?;vu_wObk9J}A!bhW)(oN46>wJkxg-}|$Ot8Kf+ znSZ;d`!1K+o0715Wn3Ftxoe4y^42J)e`P4(Y)wue)X`UV;eq>i66bGPg1&?1(p#2VPA{cdw0q|SfZe4)Ro z3P17+?NQ3={AVS-xn(MM-r3r$=8vk?#VZfUrE7T!x11w4bw7$?8Xi9^=68MI?^2zH zJc^?u1Q$q*fw}^t^?GX(&bjHwNP$0?8IQnqq?!vrp@Qa1B)BWj4|Ka-)D^MzhMPq! z17afFH+BE|&NP=I|8%1t=I%C=Pcn6ullnH~3lLj87r`}2%?**wY>d!X_sGRqv7Z+@ zJeMSy3?>Zt{@kzWU{_-mnb&OLqtnH^`!-*k;-xffjOR-C2B!WA&cc$4l6UrP=6C+= zQ^fiH0q-Ftlx)PK4y3w2xe}g#aND6h!cpzDx;zQ7PX)5_ar?)zZLu_#-(OFV8 zrY=WjUC53@kROfVGe6`df4%9y?t`1^-`Eucp=Y-gwn+tew!%iK8*d~%?lzcbV%vbx z`0t-+f?1ZkCwnBx5FBQW9d)br7lR+SLRnmD+;oNa#rO1UsfrevZSw*D?i%~&-F^41 zb;t_JLO(1sP~vPdIZDXJfObqgWA4QuToM(_%>O`ie?PA?=)-a<{aV9$g7AQS5i;QS z0n61%K<0gs49`2jnWz{SN0MzU-W@WC<+Gv%Ld(Cp34ge#agoFM zWV-Hz2t^Iv$s>dW9f&(yffq9X*ohcvBNNJik}dmTUJLN9(ml4WKVP5n-=4TlBj&zq zWT_L%P?Gx37(GC|zm(Y8IcsG)`P3587hyrZIrrfaK9q9-OFF_IvJW)No6EXql;f37 zGC&_dy?JI2>_u9omf=qu1O8BdjFsDQ+Rn))Cx2!JqP(;HRJ(*GRIwam5chUv6 zHhOA2ssvdkpT52a)b>4hoHy#R3V_w-_;jy!@8G}BO354OiiF|@+uRjO8~5a3XswDs zhL>kz@p8nu{?_PBvp=mi8q%>|y##$72*2$gJ_8wQ8w@TkE}wA|8)_^dT(27{FxUiE zILyk@ARy1bpCm|vkV{>-c+vOIOAz`=V2ug}y0{Am2M2fl-6Zs9$P7b+Ssypeus2M< zEKOl}4vn-zD9iNcEp6WcH_D}dQ4s(iCh=!=#N#3?yt%cfu9wNkuqsP}a&m4U^qES+ zo09ovF~Ffb3%rTaXQwBni`}6x3P3oo2jKZ13bX&rk70IDa}4qgVYcKx2@AbpYol0j zyvVNog#}!a9UQ}WT#`F(60)lLa6%ktX12RU$6;J&5n4ox(s9xi(^o=kW#`;LK7iNa zLyx&Uawh6OmuI3uLQ+2`BDYtF2@xVcz1rUqm7%(jcXyeRDAQttSIs%{cmo$;V0t*a zuU`agY5G2!oIl9V>(qD%qNvZ-x0}1Xmy;CfofP_ixKUmE!qqSlaKS_g+k^ zL?}oyv$HS8Q%a-|02#`Zf++7CD2+%@)_C}KijuNF4nlY#V9|&JsTU`la8}dM!0fz~ zqKpk36l{lk>vVsFH~w>jB_UuSNS%NugQ1t8VNCfB_AnZZgQAc7SfGh&2H5_v{*2QS_<>&b*3$+IgfD-{AK@?o@)ns)_>VfftY?T`?)-VCz>}MFr(`DCEqw$N1iL|D)7#31MvN`zY@Vw+bAY z9x;*on)!M!TykUu;Y}3~zme`_l8yxHjpS^Xb8oI^7tLBSHy&e`^#oalF@;tcZq^8BZDL=rql4>e*cY_lU!c{~04+CK_`P9{|`SE-A@;|aS|!;dVqJ#{!Bm-4m5QIJewZy^KC<_;Uu{u-SdOA1s}21s zK`0~|Ih$#ZE2BfRqNpoz_9~+<#f7ICLRpb*ef8S43%nnvDu9P#f40-|LtA&Z4BPhB znV2qc$O5}Otk1r_K0U~uUMBg`-n83TJkYVeTZ_HRc8`I`wo0c;*m$!56 zA&6gQltgn{WW3A%td8-6MpKCge|0!RX5q`B`Retn%k&di9H~k%LA)NLY^S^k7 z(t+TD%NY?ftn2pfGmUWWN87WaRZ6_yk>KpOhw2&^G*Z1}X#|%GByHs8yDEawL919|OD#TZY>O9@ ziR;_uhhS%Ap)FH#-{6j{>U*hB(zdQHJl-RO?{j0iaTBScOYc;}rt|Y`5QJ{QBGi6X%hQk?VFq2&@DR7Q~xfem-8o!*!d_!!UD}AJKb3 z#G`=2jo_h3{v{hsn9$A4@ipG~O$T+>-W`x6|?w+!U|o zA`K8nCCl@)`2T(?__}~P=g|J`Hsjs>C;L7wG}500+w0%6fAq3@>Dw(ywKPK!_nl6X zI!|ca6|?&5j#_c~uKSSpNMPnURs~tybPY#QKF>%|;^%P7a78XjGEa%ugwZH9z%^;c{PDV^$z z@K_%xh&nrP{yd=}76AmX@QoWtFukV@yA2 z)nJw?jUel}IqQ=4U}ursw6h4S`|Xa^jI0pN-N$YflAl5Uo8*S{iw&~v#RKJ$9qf;C zT?$=yIw!;)s&4ZSB{RZ(F*!$)$LsbNVm3=~W^ka{ z=pwjtLm4i|6-|W6#~JcGWuS@sCvHaQo7X_i3WzVy2cl4*Er3Lv5XF8U6g6nM1U$C~ zBPIUIL0&9i_;TA4aaHV6y+vH=2Y&0FJ9}~d#rF$MS21^)&RdWOw~H6Ymt9Nz)m5_i z9Zym?Z&!aomGqcFgmvPhh(P^i!tRBE;$X5cBPFkVbO$z(z&h&J#ZIqTBK8uJ#Uj_3GFfCPEmGt0B4vk6I-K+{`P&z()Qz_ zZaDCSYCn6G-uFqBH4#W*Yf4}=fT~e;jgpssDwy3$dzFMkQRn<7 zS3ND)jI=tt6SA?FMlw4A#q}(GVdS@sE&+Aps>$OQ(f3(YKvM$=P$1oMw0158UQyZ? z%N3ALHY(l*OgO5H9l|Pz!;w=B651Od$q$lmJ8$SA9Ha1H^Xp?s#0`RuKt=heNBAbf z3ki(Z;I#kJV8_01Hxht61`*y$Eh!-d9Y@+o10WwnbR2!t13+Jd66pD5ZjkySZ4vsh zVqT{#tss-c`BU`no6kSQ-1n{Bfit&eCmYzqfhL~Qd1LB*Ig%|nLspw_IW7p}4d1}A zYneUZ@z8hj<+?;H{;g+OLo7Y4YTl68nnYK@h6{hk9rgOPKiWA=j07r?PIMHoCju=d z@N$@Hm#ghn-{&I*nHoqiYVw4;U)`W_qv1+2nm>IK#e(APFX zU>gXm*YBZ7^O8ln&w()1P4|IJDm`>vr^<1H+?4Ri_IG4s0^1^L$H@{{&Jg{NDj?Hl zgNy)g3kl5;5h0K=tIxeTKOIQd0;IM+AX~+F{rW`|0Vrja+Zm6!E`LVUAARp`?9WEd z4rp6R0XHcD2t)6T--Dm5M=eUHfq57k4-Ewz-Hby;ony|k;fVH2FCbTwhoAz0Oc^=m~96sDL-<65hIhKaT5J;d0IO2CG&lfs$kU3z@So=K?O! z2209*f4Vu-9B!G8@?IOS#7fBbJUP^h0kDISgM(uzQ35!|d=vr#0x*Fe@=sc8$T}VN zaBwOigy6AD>I*?jak>ya)55riNv2nn1a2iA@}G0@@foyq{FPmBE#%VwZ}Cs2E+3Ye zXhnMt3E0`b#OImWElVG_d^$hG^??i_aQiYpJGTVwg@K>~Mg@e;5==z(a;!-R?>%N9 z;7=w#Ofi6qV2MjNO$UUqNOZpwv1ulG`2NEOrlDs@;tMpN5g?~wmRC&*mvGB>2`vZm zRqOdUV_w$_PH;7ly@&)YhQ^|BS`kVR$oMiWNytQvX8@=QLeuKLDnsis=O}0BFZgH% zV4d-~`7YpOe+-{eBU6u0ehW3I>m{O9Z2GhC2hiGr$kbGyJO{4h>Tf_U*aJ=zzsu8% za?J&VtRNtvp_?^m4?XSARB;AVvH@5ma$-`m)RM;3)D+>q#po=hx=6-;=}*4Z18Oah zeB-}qqr*Elyv#1o{OFj68d9G-C*ubWl1?ep8c!bQ+=RaL2{2S{IU+(e)+|*lOAcGQT zh^hm{ov;ZwfDLToh}OV+Q7>e25e$iZ)dp#;ZF@7b3Q0h(54OzE0z7l_IZk?-2-6`%d%;bP z?;uLAib;ZKrsI?DTMFB2<5X{TJq5u0qfiJ2M&lT3MU1(r>~g2G)PBU{>=o~umeAQc zT!DyX{!bnW*MRr(W?OKD%P=7yk>i8IN1B=?IY_{e#Ao$xlpN@4KPMz7Cv&lD0^R={ z`=c*R{A0Jw26A3;mQJ{4YwPOLaq|Fz%kwPkM#A?R+n@KGfMfLPtR{TL+y2wVcJZGt z`7O4;{gvobz~bHVIWNy?d3kx9)1c+U_BUGy#mcGpEbVm$9dto|>eVSQepdnxSfc&t z(IQKX9ADi13XsHEgFQBnM}Dhr2$xQDOsCTEv$m@0>=V!t6h*+vH3$Yq7mRNpnrLI7 zf8_+40c((|sB)Xjxxi+4Jm@eETE%7HX-o1DS%GSR(#HWOsw~j#aL^eHZ`NM}ZOd_R ziGt08puDE@(BmC9farT54aqwTl7i!l-!m#TX`YZkO!3bQSp&ORhNN-a+4z zPhcLjBzH|hTW8k*o*M(2?;&`le1Y-Pr%!vCiEPFabAl2ma4mM63~r2;B4&CGXml4W z$I5z7z(tP%*LR%g$zI5E#Tp3S%+Ufd^VQjJpvE@&I{M~8<>R*iLUw=7$>BE0xF4r~ z2phrrW3F{bgO`hzUNqHaHy)J zXrZKBn$H@wmpYzH9o?8R$xUp`rvoRWeYrT78 zG&%^KSBb?ieP_IdKoa_rU&u%RhD5}y$Zua62Es229Z5oDU_>wQQXwKifG}YusDi7L zfhZ^6B?gd3Do{Ox17e^wK{>vPgF;}Lv{rGnx(bKo4N$WA(_RkbO7Kb{*0DJnp!tpW_hFw2O}%#cB3Gpr4xcRHsnR91QizkkB!Yyswf z19wy%s0~;|pmle5D+$!NgBL`Etx}~zH{g}~ho0=<8)F3>uH}}`QzB~0!IBNaPfFz9 zvhQytpM~*Fy5C+cm;nXBdQig&6xkg*2YP>qW)^HyL^={FRDf(>MfWM9yj*1R9T7fF z0g+o6yP-0hG>Y4xiiyl}4JVWyFhD4SH@O6yBqG7WC=<~Hx}q@}RP=s8vb~n1Oo$Um zfV-95OVbVzYS=%xAyN~83nNNMR901lh`fO#aD>+*^`uEn4BB;sXPldx8$*Hj8$fO^ zKywf!4!p{uC)=vu`5^SSUmYPSFE3}idUf-J#<5S16%o7u?Z0{;Yevhg6$3e!2k-%y z%8__Sb1_OCL7x?1sM=r(V#t=xopbkA|fjSfa+uS z&1TJFQxbTkHRzY=gOmj#wFq7ochCXCI=6jMyC~>K{^v1H$$SI8+6fp=mcHLUeHv(da2xhIY3>|3rGz`tdJ%Z2*3_BJG7H@i;?Rg>EjGmsN5khPH?mIso0#A4&{FxIJ zB*iUi$c3G6p%Bpj`S2HJP{rw~a4?GokfKC2=O)Db4XxHhOPdE8f<1Akh_&9}A3=ba{8G75$-(|K%19YnAKS309~80^V&w;-iK5;)-9N1<+Lmt3-{-XTT3 z2Z2nZEo`+1E0B1o=<4DF(F5$KyN0E!V^J|ru2Zw}Q09t)a z$mKE%==dzDk-ZK#kOsIv@)Fck?b!(j1YwA{Ew#sXZ%d6*vM4-n9T=kyvvARyMNqCt z_Bt&$nWUw*>)=Gh*dX7S@H+EAMutVs7?v?2VX^@MB_k9W5mD=d`&po60A%kwohOKh zF)T*3%$V!kNE+z6w-yej29bp;+ZoVSm`t#$3BI0JMFi!lyYOG9>+An2L>WIU`v3+P zT(TZiAwb!3VWPTn6Hvp&Am->GRzQ}bWDmQ`+t1#Bh4GWvU0woAa2gh99k|D}!s{Fy zt-y&N*3bYEG!9a_+XsK<&Z~Xz(WAKXE?B>yKM(@ZO1%v=gt5F9BYz4cko}akC-GHbxBTh9@+!L}eeNc=6171$78uwB32SUmV76f~wVK(G+A z;cRjuDNsh`nFY!#yTVl-EUV$Uh6pqwg7c3gRqAM@F{z!TWNhl6JvJVrxr{|fLybkE z)^kV2=5gJf^JH_XXi|hEer+|F*oI0@8KqejlXT-(`(9(5!{~dlw?1SfwF8`)HIT-r>{Za(Pg%J7&p5GF}NG;9>&ArF0wuLPavFQm< z_q>dt5z?P&3LYxAV;#uV35Be+S++o(PG@ivG(HTPLrCV?8UB8y8a$}=JPE@Mgz&v{ z%p$Y>_schL0w7nXh`UQ7g=DrEC`6nDQ4j!&1#@%RvzE@XbhM2Ya+(Z@H_yOzlmC>C zzXuH^=DbvhdYZDb-Wn##e7u4LUg}s$vpQNDxd?DjR%xz>htj3N4}#qgO+e}q48Ie! z*qEvp-#+4jrBVgOSZR?XVkKchsC3>1i~+O(10je`)2;R7%)29Le?PqyCP-tG+d9M(ZZ z8hRe0dR_oMJ5{}(^RCa$8IFOx;ZU_3A3?PfQs|W?&4ARquPy&oVqzGgC=b9eQp!Mt zogo~e_yziQ+P-<}jX!OX(sXN?&8%MOXy>@BqT(M&rd#wVr~ZAAGMgVPf)vO-%4R~Y z>*uDp?X3ZxcfVtQmd5vLgdNXMQR|*&Q3ju4^lOQiGOq}-5wj{bK`pl<5PuZJ&-5QK z#aj&JqX8t{g;uCaodaV@cdIWd=S!XrXg0amj6Z=8_O&}n7K5@MYdYS^z40Hz@t#Uj9BF?99vT=W{a(x2^?Z;Iy z3CVr}9Wv}s=;+?+QDvn+!T_oZ_whkTVv<;4moGNm_JT>|g7$2PiG^+ms-GgFhYO4d z!ZrV$`W5W{Sp%IxB!(9TDU%InVWiDy6QW!46Hp~x7_Thdbg_UAi5H+W12O0=h$Azz zE})RIOP9#rUzJcI0a9wb9_b_rI8Ka*S!H!c{Rr%Ct8cpmZ4!cSKy zZ#+1vpFrSHa7yA3;2??u$epx!sz^vkR6m=FG`^I6W$M-!)%P0W{;$zUB(a)jH>VIu z#E#D2*6M0%Q_wtPv2oRh64B6vHUWCmuP4+2;#a&u=!|@)^q*M@ir@jmxB%Mk1h^6V z;C4IiQmP4Ir_Ha6BPze3TNM=*^#Xv-DUdAlZq~1dDB1@yPrakv)tApGpyG^7;+Q4h z*RNkUK+h_^%hMiOdoCzufMg=A+Lho(okpD?*kalbGQvMW_62}9@$L2Jh!_mgjM| z*CD?JusCkByjlMaw2=T$i-o&qC(ocbHX1z!qVO_g&U-~m@B|Vke**N|#$DW9D#&jZ?q77fM$BDS{ zLS#R+<67XdHQ(5k&TZI6p!r$jN<7aJ=wk74=^#CIErN}v#WSx|p>ttnypk`SNfz!c z%Hv$`B`uzM(m*P1mmB)G&eJ$LUXVO@?U)`{gIVjhFCR^+^dpeiVNpyZFBIojd-m)Z z(*KgiCFv|oi&V5g2Y2V`d9~;90mv!WAZw<7%%dP_lGo`0vPB5cq3a+3mK`L@VRb(7 zf@?s8ZPmcy}4W|FL6Q9;_=4?uxP!(j=?tT*+C=Z;SH2U}-_)0{A|X6i7&*Bh5n) z$Mr1;!HVdGRS>%=(*Ozd)ZWpH=L7-eMbZx*L~q)3o;-N-3=vaXTwKf_T?IHx={`j` zU|fS#etN}P2pHegB#C5x2qMz^7P2GoGnC2esve3(&6ye(92D|dj9 ziGJQpQBLl~4H*ecdmXF3ocUICOjYIn>1P?@3gS}Az6w!O`M2NuWEMPeU1Ub)A@FsI zK7^WxH|KO%QnP#L3+akAXrj+RFe>oD`+~YUC+zt#0AT-%3u}SA(q^lZkCBs;5XsNQ zyhI5+G(XPYd0kZ{jAYaEpUT!MmO&`{B0YvD#6+<*5I9pvU|*u-#s<5>8OI9B#;g#1 zGeQ^#0XP)N9On5xK!A;dgJWGe?%;FL)JGIrIm214%)S^hv9Ra^V#+y7h>K0ZsgFU8 z*ZS1B6%oHhe}Mw&CQex7c6{JnkW?E39StCEbO8YYqymSmZinRo!H>dF7%(ucCMKl0 zp-zmQjZPYRDZ9bqJEH|e?b4)vz|kVSKjn+GyCEXFfP0`|TEw}7rmffbuzrKSVCzaN zD5X+^>YM+!pAASE07Bl$h0#J|d?b~32Q**0juDmUJQM5;1Nbr~#6!c zC?u={K}*m?w;4JgH69kC&l*M}M0ySjuL+VTq>ONdvI$ZKG4fwlZSO?f!Xkwnmi$1w z5b%3segBD_4L4D!@&7iyomqqj^_ukwu z+8#)0^%iU$!mE*szHG9yO>^IF=te`5Au*1llx(zGMsj%g=hHVaF^oWq$LKDa{5%IL zbewLx=20;*s=wT^z9WKpDvuwVi+WVs>yWwat`NeaR!Z8+FDNJ|ZX%5|Yj>_<;r-{U z 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 literal 0 HcmV?d00001 diff --git a/services/apiCache.ts b/services/apiCache.ts new file mode 100644 index 0000000..b085333 --- /dev/null +++ b/services/apiCache.ts @@ -0,0 +1,46 @@ +interface CacheItem { + data: T; + timestamp: number; +} + +interface CacheConfig { + globalMarketData: number; // 5 minutes + topCryptos: number; // 5 minutes + historicalData: number; // 30 minutes +} + +class APICache { + private cache: Map>; + private config: CacheConfig; + + constructor() { + this.cache = new Map(); + this.config = { + globalMarketData: 5 * 60 * 1000, // 5 minutes + topCryptos: 5 * 60 * 1000, // 5 minutes + historicalData: 30 * 60 * 1000 // 30 minutes + }; + } + + get(key: string, category: keyof CacheConfig): T | null { + const item = this.cache.get(key); + if (!item) return null; + + const ttl = this.config[category]; + if (Date.now() - item.timestamp > ttl) { + this.cache.delete(key); + return null; + } + + return item.data; + } + + set(key: string, data: T): void { + this.cache.set(key, { + data, + timestamp: Date.now() + }); + } +} + +export const apiCache = new APICache(); \ No newline at end of file diff --git a/services/binanceApiService.ts b/services/binanceApiService.ts new file mode 100644 index 0000000..cebaf30 --- /dev/null +++ b/services/binanceApiService.ts @@ -0,0 +1,83 @@ +import axios from 'axios'; + +const BINANCE_API_URL = 'https://api.binance.com/api/v3'; + +interface BinanceKline { + openTime: number; + open: string; + high: string; + low: string; + close: string; + volume: string; + closeTime: number; + quoteAssetVolume: string; + trades: number; + takerBuyBaseAssetVolume: string; + takerBuyQuoteAssetVolume: string; + ignored: string; +} + +/** + * Convert symbol to Binance trading pair format + */ +const getBinancePair = (symbol: string): string => { + // Convert common symbols to Binance format (BTCUSDT, ETHUSDT, etc.) + return `${symbol.toUpperCase()}USDT`; +}; + +/** + * Get historical price data from Binance + */ +export const fetchHistoricalPriceData = async (symbol: string, days: number = 30): Promise => { + try { + const interval = days <= 7 ? '1h' : '1d'; + const limit = days <= 7 ? days * 24 : days; + + const pair = getBinancePair(symbol); + + const response = await axios.get(`${BINANCE_API_URL}/klines`, { + params: { + symbol: pair, + interval, + limit: Math.min(limit, 1000) // Binance has a limit of 1000 candles + } + }); + + if (!response.data || !Array.isArray(response.data)) { + throw new Error('Invalid response from Binance API'); + } + + // Transform to the format our charts expect - using closing price (index 4) + const prices = response.data.map((kline: any) => [kline[0], parseFloat(kline[4])]); + + // Use actual traded volume for better accuracy + const volumes = response.data.map((kline: any) => [kline[0], parseFloat(kline[5]) * parseFloat(kline[4])]); + + // For market caps, we estimate based on circulating supply + const market_cap_multiplier = getMarketCapMultiplier(symbol); + const market_caps = prices.map(([time, price]) => [time, price * market_cap_multiplier]); + + return { + prices, + market_caps, + total_volumes: volumes + }; + } catch (error) { + console.error('Error fetching data from Binance:', error); + throw error; + } +}; + +/** + * Helper to estimate market cap based on approximate circulating supply + */ +function getMarketCapMultiplier(symbol: string): number { + // Rough estimates of circulating supply for major coins + switch (symbol.toLowerCase()) { + case 'btc': return 19500000; // ~19.5M BTC in circulation + case 'eth': return 120000000; // ~120M ETH in circulation + case 'bnb': return 153000000; // ~153M BNB in circulation + case 'sol': return 430000000; // ~430M SOL in circulation + default: return 100000000; // Default fallback + } +} diff --git a/services/coinMarketCapService.ts b/services/coinMarketCapService.ts new file mode 100644 index 0000000..6033bc9 --- /dev/null +++ b/services/coinMarketCapService.ts @@ -0,0 +1,286 @@ +import axios from 'axios'; +import { apiCache } from './apiCache'; + +// Configuration +const MAX_RETRIES = 3; +const INITIAL_RETRY_DELAY = 1000; // 1 second +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes +const API_PROXY_URL = '/api/coinmarketcap'; + +// Interface definitions +export interface GlobalMarketData { + total_market_cap: { [key: string]: number }; + total_volume: { [key: string]: number }; + market_cap_percentage: { [key: string]: number }; + market_cap_change_percentage_24h_usd: number; + active_cryptocurrencies: number; + markets: number; + last_updated: string; +} + +export interface TokenData { + id: number; + name: string; + symbol: string; + slug: string; + cmc_rank: number; + quote: { + USD: { + price: number; + volume_24h: number; + market_cap: number; + percent_change_1h: number; + percent_change_24h: number; + percent_change_7d: number; + percent_change_30d: number; + last_updated: string; + } + } +} + +// Helper functions +async function retryWithBackoff( + operation: () => Promise, + retries = MAX_RETRIES, + delay = INITIAL_RETRY_DELAY +): Promise { + try { + return await operation(); + } catch (error: any) { + if (retries === 0 || (error.response && error.response.status !== 429)) { + throw error; + } + + console.warn(`Request failed, retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + + return retryWithBackoff(operation, retries - 1, delay * 2); + } +} + +// Data conversion functions +const convertToGlobalMarketData = (response: any): GlobalMarketData => { + try { + console.log('API Response:', JSON.stringify(response, null, 2)); + + if (response.status && response.status.error_code) { + throw new Error(`API Error: ${response.status.error_message}`); + } + + const data = response.data || response; + + return { + total_market_cap: { + usd: data.total_market_cap?.usd || + data.quote?.USD?.total_market_cap || + data.total_market_cap || 0 + }, + total_volume: { + usd: data.total_volume?.usd || + data.quote?.USD?.total_volume_24h || + data.total_volume || 0 + }, + market_cap_percentage: { + btc: data.market_cap_percentage?.btc || + data.btc_dominance || + data.dominance?.btc || 0, + eth: data.market_cap_percentage?.eth || + data.eth_dominance || + data.dominance?.eth || 0, + }, + market_cap_change_percentage_24h_usd: + data.market_cap_change_percentage_24h_usd || + data.quote?.USD?.total_market_cap_yesterday_percentage_change || 0, + active_cryptocurrencies: + data.active_cryptocurrencies || + data.total_cryptocurrencies || 0, + markets: + data.markets || + data.active_market_pairs || 0, + last_updated: + data.last_updated || + new Date().toISOString() + }; + } catch (error) { + console.error('Error converting global market data:', error); + return getSimulatedGlobalData(); + } +}; + +const convertToStandardTokenData = (tokens: TokenData[]): any[] => { + try { + return tokens.map(token => { + if (!token.quote || !token.quote.USD) { + throw new Error(`Invalid token data structure for token: ${token.id}`); + } + + return { + id: token.id?.toString() || '0', + name: token.name || 'Unknown', + symbol: token.symbol || 'N/A', + current_price: token.quote.USD.price || 0, + market_cap: token.quote.USD.market_cap || 0, + total_volume: token.quote.USD.volume_24h || 0, + price_change_percentage_24h: token.quote.USD.percent_change_24h || 0, + price_change_percentage_7d: token.quote.USD.percent_change_7d || 0, + price_change_percentage_30d: token.quote.USD.percent_change_30d || 0, + market_cap_rank: token.cmc_rank || 0 + }; + }).filter(token => token.name !== 'Unknown'); + } catch (error) { + console.error('Error converting token data:', error); + return []; + } +}; + +// API functions +export const fetchGlobalMarketData = async (): Promise => { + const cachedData = apiCache.get('globalMarketData', 'globalMarketData'); + if (cachedData) { + return cachedData; + } + + try { + const response = await retryWithBackoff(async () => { + return await axios.get(`${API_PROXY_URL}/global-metrics`); + }); + + const data = convertToGlobalMarketData(response.data); + apiCache.set('globalMarketData', data); + return data; + } catch (error) { + console.error('Error fetching global market data:', error); + const fallbackData = getSimulatedGlobalData(); + apiCache.set('globalMarketData', fallbackData); + return fallbackData; + } +}; + +export const fetchTopCryptocurrencies = async (limit: number = 10): Promise => { + const cacheKey = `topCryptos_${limit}`; + const cachedData = apiCache.get(cacheKey, 'topCryptos'); + if (cachedData) { + return cachedData; + } + + try { + const response = await retryWithBackoff(async () => { + return await axios.get(`${API_PROXY_URL}/listings`, { + params: { + limit, + sort: 'market_cap', + sort_dir: 'desc', + } + }); + }); + + const data = convertToStandardTokenData(response.data.data); + apiCache.set(cacheKey, data); + return data; + } catch (error) { + console.error('Error fetching top cryptocurrencies:', error); + const fallbackData = getSimulatedTopTokens(limit); + apiCache.set(cacheKey, fallbackData); + return fallbackData; + } +}; + +export const fetchHistoricalData = async ( + symbol: string, + days: number = 30, + interval: string = 'daily' +): Promise => { + const cacheKey = `historicalData_${symbol}_${days}`; + const cachedData = apiCache.get(cacheKey, 'historicalData'); + if (cachedData) { + return cachedData; + } + + try { + // Try to get data from Binance API if possible + const binanceData = await retryWithBackoff(async () => { + const binanceApi = await import('@/services/binanceApiService'); + return await binanceApi.fetchHistoricalPriceData(symbol, days); + }).catch(() => null); + + if (binanceData) { + apiCache.set(cacheKey, binanceData); + return binanceData; + } + + // Fallback to simulated data + console.warn(`Using simulated data for ${symbol} historical prices`); + const fallbackData = getSimulatedHistoricalData(symbol, days); + apiCache.set(cacheKey, fallbackData); + return fallbackData; + } catch (error) { + console.error(`Error fetching historical data for ${symbol}:`, error); + const fallbackData = getSimulatedHistoricalData(symbol, days); + apiCache.set(cacheKey, fallbackData); + return fallbackData; + } +}; + +// Simulated data generators +function getSimulatedGlobalData(): GlobalMarketData { + return { + total_market_cap: { + usd: 2300000000000 // $2.3T + }, + total_volume: { + usd: 115000000000 // $115B + }, + market_cap_percentage: { + btc: 48.5, + eth: 17.3, + }, + market_cap_change_percentage_24h_usd: 2.34, + active_cryptocurrencies: 10423, + markets: 814, + last_updated: new Date().toISOString() + }; +} + +function getSimulatedTopTokens(limit: number): any[] { + const mockTokens = [ + { id: '1', name: 'Bitcoin', symbol: 'btc', current_price: 64253.12, price_change_percentage_24h: 2.41, market_cap: 1260000000000, market_cap_rank: 1 }, + { id: '2', name: 'Ethereum', symbol: 'eth', current_price: 3427.81, price_change_percentage_24h: 1.58, market_cap: 412000000000, market_cap_rank: 2 }, + { id: '3', name: 'Tether', symbol: 'usdt', current_price: 0.9998, price_change_percentage_24h: 0.01, market_cap: 110000000000, market_cap_rank: 3 }, + { id: '4', name: 'BNB', symbol: 'bnb', current_price: 587.33, price_change_percentage_24h: 0.92, market_cap: 85000000000, market_cap_rank: 4 }, + { id: '5', name: 'Solana', symbol: 'sol', current_price: 143.38, price_change_percentage_24h: 3.76, market_cap: 65000000000, market_cap_rank: 5 }, + ]; + + return mockTokens.slice(0, limit); +} + +function getSimulatedHistoricalData(symbol: string, days: number): any { + const basePrice = symbol.toLowerCase() === 'btc' ? 68000 : + symbol.toLowerCase() === 'eth' ? 3500 : + symbol.toLowerCase() === 'sol' ? 140 : + symbol.toLowerCase() === 'bnb' ? 600 : 100; + + const prices = []; + const market_caps = []; + const total_volumes = []; + + const now = Date.now(); + const oneDayMs = 24 * 60 * 60 * 1000; + + let currentPrice = basePrice; + + for (let i = days; i >= 0; i--) { + const timestamp = now - (i * oneDayMs); + const randomChange = currentPrice * (Math.random() * 0.06 - 0.03); // -3% to +3% + currentPrice += randomChange; + + prices.push([timestamp, currentPrice]); + market_caps.push([timestamp, currentPrice * 19500000]); // Simulated market cap + total_volumes.push([timestamp, currentPrice * 500000 * (0.7 + Math.random() * 0.6)]); // Simulated volume + } + + return { + prices, + market_caps, + total_volumes + }; +} From c7536b636744a97a69c83649edb5c889449633b1 Mon Sep 17 00:00:00 2001 From: Minh Duy - Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:57:55 +0700 Subject: [PATCH 078/107] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 28024fe..2f57433 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ CryptoPath is a blockchain transaction visualization system that simplifies bloc - Leverage a graph database (currently using static demo data) for efficient data storage and retrieval. ## Team Members -- **Le Nguyen Dang Duy** (105028557) - Frontend Lead / Graph Visualization -- **Phan Cong Hung** (104995595) - Backend & Data Integration Lead -- **Nguyen Minh Duy** (104974743) - Full-Stack Developer / UI & UX +- **Le Nguyen Dang Duy** (105028557) - **Frontend Developer / Graph Visualization** +- **Nguyen Minh Duy** (104974743) - **Team Leader / Full-Stack Developer / Product Experience Architect** +- **Phan Cong Hung** (104995595) - **Backend & Frontend Developer / API Integration** ## Project Structure ### Frontend From 54f71e4a46cf6ce481ced1b989eb43849f550205 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:05:03 +0700 Subject: [PATCH 079/107] Add NFT layout component with navigation and breadcrumb support --- app/NFT/layout.tsx | 136 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 app/NFT/layout.tsx diff --git a/app/NFT/layout.tsx b/app/NFT/layout.tsx new file mode 100644 index 0000000..7bae892 --- /dev/null +++ b/app/NFT/layout.tsx @@ -0,0 +1,136 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { LayoutGrid, Layers, Bookmark, Activity, ChevronRight, Info } from 'lucide-react'; + +export default function NFTLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + const [mounted, setMounted] = useState(false); + + // Determine active section based on URL + const isMarketplace = pathname === '/NFT'; + const isCollections = pathname.includes('/NFT/collection'); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + return ( +

+
+ {/* Navigation Tabs */} +
+
+ + + + + + +
+
+ + {/* Breadcrumb Navigation */} + + + {/* Info Banner */} + {isMarketplace && ( +
+ +
+

About PATH Marketplace

+

+ This marketplace is exclusive to CryptoPath ecosystem NFTs. Connect your wallet to start trading PATH NFTs. + You'll need PATH tokens for transactions, which you can get from our Faucet. +

+
+
+ )} + + {/* Page Title */} +

+ {isMarketplace ? ( + <>PATH NFT Marketplace + ) : ( + <>NFT Collections Explorer + )} +

+ + {/* Main Content */} +
{children}
+
+
+ ); +} From 3a37eec1c031627686cfa0af58590c576d0a1fc8 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:06:51 +0700 Subject: [PATCH 080/107] Enhance NFT components with improved image loading, validation, and new market stats display --- app/NFT/page.tsx | 492 +++++++++++++++---------- components/NFT/FeaturedCollections.tsx | 181 +++++++++ components/NFT/ListForm.tsx | 70 +++- components/NFT/MintForm.tsx | 130 ++++++- components/NFT/NFTCard.tsx | 36 +- components/NFT/NFTDetailsModal.tsx | 79 ++-- components/NFT/NFTMarketStats.tsx | 90 +++++ components/NFT/NFTNavigation.tsx | 343 ++++++++++++++--- components/NFT/PriceChart.tsx | 146 ++++++++ components/NFT/TradingHistory.tsx | 273 ++++++++++++++ components/NFT/WhitelistForm.tsx | 144 +++++++- lib/api/alchemyNFTApi.ts | 170 ++++++--- next.config.js | 2 +- 13 files changed, 1775 insertions(+), 381 deletions(-) create mode 100644 components/NFT/FeaturedCollections.tsx create mode 100644 components/NFT/NFTMarketStats.tsx create mode 100644 components/NFT/PriceChart.tsx create mode 100644 components/NFT/TradingHistory.tsx diff --git a/app/NFT/page.tsx b/app/NFT/page.tsx index fea0126..4221c81 100644 --- a/app/NFT/page.tsx +++ b/app/NFT/page.tsx @@ -8,7 +8,9 @@ import MintForm from '@/components/NFT/MintForm'; import WhitelistForm from '@/components/NFT/WhitelistForm'; import { useWallet } from '@/components/Faucet/walletcontext'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import ParticlesBackground from '@/components/ParticlesBackground'; +import NFTMarketStats from '@/components/NFT/NFTMarketStats'; +import PriceChart from '@/components/NFT/PriceChart'; +import { toast } from '@/hooks/use-toast'; // Contract addresses const NFT_CONTRACT_ADDRESS = "0xdf5d4038723f6605A3eCd7776FFe25f3b1Be39a0"; @@ -53,6 +55,13 @@ interface NFTData { listings: any[]; } +interface NFTMetadata { + name: string; + image: string; + description?: string; + [key: string]: any; +} + export default function NFTMarketplace() { const { account, connectWallet } = useWallet(); const [activeTab, setActiveTab] = useState<'market' | 'owned' | 'listings' | 'mint' | 'whitelist'>('market'); @@ -67,6 +76,16 @@ export default function NFTMarketplace() { listings: [] }); const [isInitialLoad, setIsInitialLoad] = useState(true); + + // Market statistics + const [marketStats, setMarketStats] = useState({ + totalVolume: '12,450.35', + dailyVolume: '1,245.62', + avgPrice: '125.75', + listedCount: 48, + soldCount: 152, + priceChange: 8.5 + }); const isOwner = useMemo(() => account?.toLowerCase() === ownerAddress.toLowerCase(), @@ -118,18 +137,49 @@ export default function NFTMarketplace() { }, [account, checkWhitelistStatus]); // Fetch PATH balance - const fetchPathBalance = useCallback(async (account: string) => { + const fetchPathBalance = useCallback(async (address: string) => { try { const provider = getProvider(); const tokenContract = new ethers.Contract(PATH_TOKEN_ADDRESS, TOKEN_ABI, provider); - const balance = await tokenContract.balanceOf(account); + const balance = await tokenContract.balanceOf(address); setPathBalance(parseFloat(ethers.utils.formatUnits(balance, 18)).toFixed(4)); } catch (error) { console.error("Error fetching PATH balance:", error); } }, []); - // Fetch NFT data + // Fetch metadata with timeout and retry + const fetchMetadata = async (uri: string, retries = 3): Promise => { + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Metadata fetch timeout')), 5000) + ); + + for (let i = 0; i < retries; i++) { + try { + const response = await Promise.race([ + fetch(uri), + timeout + ]) as Response; + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (!data.name || !data.image) { + throw new Error('Invalid metadata format'); + } + + return data as NFTMetadata; + } catch (error) { + if (i === retries - 1) throw error; + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + } + } + throw new Error('Failed to fetch metadata after retries'); + }; + + // Fetch NFT data with improved error handling const fetchNFTs = useCallback(async () => { if (!account) return; @@ -139,8 +189,8 @@ export default function NFTMarketplace() { const listedIds = await contract.getAllListings().catch(() => []); - // Market NFTs - const marketNFTs = await Promise.all( + // Market NFTs with improved error handling + const marketNFTs = await Promise.allSettled( listedIds.map(async (id: ethers.BigNumber) => { try { const [uri, listing, owner] = await Promise.all([ @@ -148,7 +198,9 @@ export default function NFTMarketplace() { contract.listings(id), contract.ownerOf(id).catch(() => '0x0') ]); - const metadata = await fetch(uri.toString()).then(res => res.json()); + + const metadata = await fetchMetadata(uri); + return { id: id.toString(), ...metadata, @@ -164,46 +216,67 @@ export default function NFTMarketplace() { }) ); - // Owned NFTs + // Owned NFTs with improved pagination const totalSupply = await contract.totalSupply().catch(() => ethers.BigNumber.from(0)); + const pageSize = 20; // Process in smaller chunks const allIds = Array.from({ length: totalSupply.toNumber() }, (_, i) => i); - - const ownedNFTs = await Promise.all( - allIds.map(async (id) => { - try { - const [owner, listing] = await Promise.all([ - contract.ownerOf(id).catch(() => '0x0'), - contract.listings(id) - ]); - if (owner.toLowerCase() === account.toLowerCase() && !listing.isListed) { - const uri = await contract.tokenURI(id); - const metadata = await fetch(uri.toString()).then(res => res.json()); - return { - id: id.toString(), - ...metadata, - owner: owner, - isListed: false - }; + const ownedNFTs = []; + + for (let i = 0; i < allIds.length; i += pageSize) { + const pageIds = allIds.slice(i, i + pageSize); + const pageResults = await Promise.allSettled( + pageIds.map(async (id) => { + try { + const [owner, listing] = await Promise.all([ + contract.ownerOf(id).catch(() => '0x0'), + contract.listings(id) + ]); + + if (owner.toLowerCase() === account.toLowerCase() && !listing.isListed) { + const uri = await contract.tokenURI(id); + const metadata = await fetchMetadata(uri); + + return { + id: id.toString(), + ...metadata, + owner: owner, + isListed: false + }; + } + return null; + } catch (error) { + console.error(`Error processing owned NFT ${id}:`, error); + return null; } - return null; - } catch (error) { - return null; - } - }) - ); + }) + ); + + ownedNFTs.push(...pageResults + .filter(result => result.status === 'fulfilled' && result.value !== null) + .map(result => (result as PromiseFulfilledResult).value) + ); + } + + const validMarketNFTs = marketNFTs + .filter(result => result.status === 'fulfilled' && result.value !== null) + .map(result => (result as PromiseFulfilledResult).value); setNftData({ - market: marketNFTs.filter(nft => nft !== null), - owned: ownedNFTs.filter(nft => nft !== null), - listings: marketNFTs.filter(nft => - nft !== null && - nft.isListed && + market: validMarketNFTs, + owned: ownedNFTs, + listings: validMarketNFTs.filter(nft => + nft.isListed && nft.seller?.toLowerCase() === account.toLowerCase() ) }); setIsInitialLoad(false); } catch (error) { console.error("Error fetching NFTs:", error); + toast({ + title: "Error", + description: "Failed to fetch NFTs. Please try again later.", + variant: "destructive" + }); } }, [account]); @@ -259,12 +332,24 @@ export default function NFTMarketplace() { const signer = provider.getSigner(); const contract = new ethers.Contract(NFT_CONTRACT_ADDRESS, NFT_ABI, signer); + // Validate the token URI before minting + await fetchMetadata(tokenURI); + const tx = await contract.mintNFT(recipient, tokenURI); await tx.wait(); await refreshData(); + toast({ + title: "Success", + description: "NFT minted successfully!", + variant: "default" + }); } catch (error) { console.error("Minting failed:", error); - alert(error instanceof Error ? error.message : "Mint failed"); + toast({ + title: "Error", + description: error instanceof Error ? error.message : "Mint failed", + variant: "destructive" + }); } finally { setProcessing(false); } @@ -273,7 +358,11 @@ export default function NFTMarketplace() { // Handle buy NFT const handleBuyNFT = async (tokenId: string, price: string) => { if (!account) { - alert("Please connect wallet!"); + toast({ + title: "Error", + description: "Please connect wallet first!", + variant: "destructive" + }); return; } @@ -298,9 +387,18 @@ export default function NFTMarketplace() { await tx.wait(); await refreshData(); + toast({ + title: "Success", + description: "NFT purchased successfully!", + variant: "default" + }); } catch (error) { console.error("Purchase failed:", error); - alert("Transaction failed! Check console for details."); + toast({ + title: "Error", + description: "Transaction failed! Check console for details.", + variant: "destructive" + }); } finally { setProcessing(false); } @@ -309,7 +407,11 @@ export default function NFTMarketplace() { // Handle list NFT const handleListNFT = async (tokenId: string, price: string) => { if (!account) { - alert("Please connect wallet first!"); + toast({ + title: "Error", + description: "Please connect wallet first!", + variant: "destructive" + }); return; } @@ -331,9 +433,18 @@ export default function NFTMarketplace() { ); await tx.wait(); await refreshData(); + toast({ + title: "Success", + description: "NFT listed successfully!", + variant: "default" + }); } catch (error) { console.error("Listing failed:", error); - alert(error instanceof Error ? error.message : "Unknown error"); + toast({ + title: "Error", + description: error instanceof Error ? error.message : "Unknown error", + variant: "destructive" + }); } finally { setProcessing(false); } @@ -342,7 +453,11 @@ export default function NFTMarketplace() { // Handle unlist NFT const handleUnlistNFT = async (tokenId: string) => { if (!account) { - alert("Please connect wallet first!"); + toast({ + title: "Error", + description: "Please connect wallet first!", + variant: "destructive" + }); return; } @@ -362,13 +477,18 @@ export default function NFTMarketplace() { const tx = await contract.unlistNFT(tokenId); await tx.wait(); await refreshData(); + toast({ + title: "Success", + description: "NFT unlisted successfully!", + variant: "default" + }); } catch (error: unknown) { console.error("Unlisting failed:", error); - alert( - error instanceof Error - ? error.message - : "Unknown error occurred during unlisting" - ); + toast({ + title: "Error", + description: error instanceof Error ? error.message : "Unknown error occurred during unlisting", + variant: "destructive" + }); } finally { setProcessing(false); } @@ -386,164 +506,150 @@ export default function NFTMarketplace() { const tx = await contract.updateWhitelist(address, status); await tx.wait(); - alert('Whitelist updated successfully!'); + toast({ + title: "Success", + description: "Whitelist updated successfully!", + variant: "default" + }); } catch (error) { console.error("Whitelist update failed:", error); - alert(error instanceof Error ? error.message : "Update failed"); + toast({ + title: "Error", + description: error instanceof Error ? error.message : "Update failed", + variant: "destructive" + }); } finally { setProcessing(false); } }; return ( -
- -
- {/* Header section */} -
-

- NFT Marketplace -

-
- {account && ( -
- - {pathBalance} - - PATH -
- )} - -
-
- - - - {!account ? ( -
- Please connect your wallet to view NFTs -
- ) : ( - <> - {isInitialLoad ? ( -
- {[...Array(8)].map((_, i) => ( -
- ))} -
- ) : ( - <> - {activeTab === 'mint' ? ( - - - Mint - - - - - - ) : activeTab === 'whitelist' ? ( - - - Whitelist Management - - - - - - ) : ( - <> -
- {paginatedData.map((nft, index) => ( -
- handleBuyNFT(tokenId, price || '0') - : activeTab === 'owned' - ? (tokenId, price) => handleListNFT(tokenId, price || '0') - : handleUnlistNFT - } - processing={processing} - /> -
- ))} -
- - {totalPages > 1 && ( -
- +
+

Trade exclusive NFTs in the PATH ecosystem using PATH tokens

+
+ + {/* Market Statistics */} + + + {/* Price Chart */} +
+ +
+ + {/* Tabs Navigation */} + + + {!account ? ( +
+ Please connect your wallet to view NFTs +
+ ) : ( + <> + {isInitialLoad ? ( +
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+ ) : ( + <> + {activeTab === 'mint' ? ( + + + Mint + + + + + + ) : activeTab === 'whitelist' ? ( + + + Whitelist Management + + + + + + ) : ( + <> +
+ {paginatedData.map((nft, index) => ( +
+ handleBuyNFT(tokenId, price || '0') + : activeTab === 'owned' + ? (tokenId, price) => handleListNFT(tokenId, price || '0') + : handleUnlistNFT + } + processing={processing} />
- )} - - )} - - )} - - )} - - {processing && ( -
-
-
-

Processing Transaction

-
- {[...Array(3)].map((_, i) => ( -
- ))} -
+ ))} +
+ + {totalPages > 1 && ( +
+ +
+ )} + + )} + + )} + + )} + + {processing && ( +
+
+
+

Processing Transaction

+
+ {[...Array(3)].map((_, i) => ( +
+ ))}
- )} -
+
+ )}
); } \ No newline at end of file diff --git a/components/NFT/FeaturedCollections.tsx b/components/NFT/FeaturedCollections.tsx new file mode 100644 index 0000000..64007dd --- /dev/null +++ b/components/NFT/FeaturedCollections.tsx @@ -0,0 +1,181 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { ExternalLink, Sparkles, Zap, TrendingUp, Users } from 'lucide-react'; + +interface Collection { + id: string; + name: string; + description: string; + image: string; + bannerImage?: string; + floorPrice?: string; + volume24h?: string; + totalItems?: number; + owners?: number; + verified?: boolean; + category: string; +} + +interface FeaturedCollectionsProps { + collections: Collection[]; +} + +export default function FeaturedCollections({ collections = [] }: FeaturedCollectionsProps) { + // If no real data, use sample data + const sampleCollections: Collection[] = collections.length > 0 ? collections : [ + { + id: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', + name: 'Bored Ape Yacht Club', + description: 'The Bored Ape Yacht Club is a collection of 10,000 unique Bored Ape NFTs— unique digital collectibles living on the Ethereum blockchain.', + image: 'https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?auto=format&dpr=1&w=1000', + bannerImage: 'https://i.seadn.io/gae/i5dYZRkVCUK97bfprQ3WXyrT9BnLSZtVKGJlKQ919uaUB0sxbngVCioaiyu9r6snqfi2aaTyIvv6DHm4m2R3y7hMajbsv14pSZK8mhs?auto=format&dpr=1&w=3840', + floorPrice: '30.5', + volume24h: '450.23', + totalItems: 10000, + owners: 6350, + verified: true, + category: 'Art' + }, + { + id: '0x60e4d786628fea6478f785a6d7e704777c86a7c6', + name: 'Mutant Ape Yacht Club', + description: 'The MUTANT APE YACHT CLUB is a collection of up to 20,000 Mutant Apes that can only be created by exposing an existing Bored Ape to a vial of MUTANT SERUM.', + image: 'https://i.seadn.io/gae/lHexKRMpw-aoSyB1WdFBff5yfANLReFxHzt1DOj_sg7mS14yARpuvYcUtsyyx-Nkpk6WTcUPF6rLh2D4Xw?auto=format&dpr=1&w=1000', + floorPrice: '10.2', + volume24h: '250.15', + totalItems: 19423, + owners: 12340, + verified: true, + category: 'Art' + }, + { + id: '0xed5af388653567af2f388e6224dc7c4b3241c544', + name: 'Azuki', + description: 'Azuki starts with a collection of 10,000 avatars that give you membership access to The Garden: a corner of the internet where artists, builders, and web3 enthusiasts meet to create a decentralized future.', + image: 'https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?auto=format&dpr=1&w=1000', + floorPrice: '8.75', + volume24h: '175.45', + totalItems: 10000, + owners: 5120, + verified: true, + category: 'PFP' + } + ]; + + return ( +
+
+

+ + Featured Collections +

+ + + +
+ +
+ {sampleCollections.map((collection) => ( + +
+ {collection.bannerImage ? ( + {`${collection.name} + ) : ( +
+ )} +
+
+ {collection.name} +
+
+ {collection.verified && ( +
+ + Verified + +
+ )} +
+ + + + + {collection.name} + + + + {collection.category} + + + + +

+ {collection.description} +

+ +
+
+

Floor Price

+

+ {collection.floorPrice} ETH +

+
+
+

Volume (24h)

+

+ {collection.volume24h} ETH + {parseFloat(collection.volume24h || '0') > 200 && ( + + )} +

+
+
+
+ + +
+ + {collection.owners?.toLocaleString()} owners +
+
+ {collection.totalItems?.toLocaleString()} items +
+ + + +
+
+ ))} +
+ +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/components/NFT/ListForm.tsx b/components/NFT/ListForm.tsx index c7a9a2d..ae13d9b 100644 --- a/components/NFT/ListForm.tsx +++ b/components/NFT/ListForm.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, FormEvent } from 'react'; export default function ListForm({ onSubmit, @@ -8,31 +8,77 @@ export default function ListForm({ onCancel: () => void; }) { const [price, setPrice] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + // Clear previous error + setError(''); + + // Validate price + if (!price) { + setError('Please enter a price'); + return; + } + + const numPrice = parseFloat(price); + if (isNaN(numPrice)) { + setError('Please enter a valid number'); + return; + } + + if (numPrice <= 0) { + setError('Price must be greater than 0'); + return; + } + + if (numPrice > 1000000) { + setError('Price cannot exceed 1,000,000 PATH'); + return; + } + + // Submit if validation passes + onSubmit(price); + }; return ( -
- setPrice(e.target.value)} - placeholder="Enter price in PATH" - className="w-full p-2 bg-gray-700 rounded text-sm" - /> +
+
+ { + setPrice(e.target.value); + setError(''); // Clear error when input changes + }} + placeholder="Enter price in PATH" + className={`w-full p-2 bg-gray-700 rounded text-sm ${ + error ? 'border border-red-500' : '' + }`} + /> + {error && ( +

{error}

+ )} +
-
+ ); } \ No newline at end of file diff --git a/components/NFT/MintForm.tsx b/components/NFT/MintForm.tsx index cfeaeb1..bcab115 100644 --- a/components/NFT/MintForm.tsx +++ b/components/NFT/MintForm.tsx @@ -16,30 +16,109 @@ export default function MintForm({ const [tokenURI, setTokenURI] = useState(''); const [recipient, setRecipient] = useState(''); const [isWhitelisted, setIsWhitelisted] = useState(false); + const [validatingURI, setValidatingURI] = useState(false); + const [uriError, setUriError] = useState(null); // Validate Ethereum address const isValidAddress = (address: string) => utils.isAddress(address); - // Validate IPFS/HTTP URI - const isValidURI = (uri: string) => uri.startsWith('ipfs://') || uri.startsWith('https://'); + // Validate metadata format + const validateMetadata = async (uri: string): Promise => { + try { + setValidatingURI(true); + setUriError(null); + + if (!uri.startsWith('ipfs://') && !uri.startsWith('https://')) { + setUriError('URI must start with ipfs:// or https://'); + return false; + } + + // For IPFS URIs, only validate the format + if (uri.startsWith('ipfs://')) { + const cid = uri.replace('ipfs://', '').split('/')[0]; + if (!cid || cid.length < 32) { + setUriError('Invalid IPFS CID format'); + return false; + } + return true; + } + + // For HTTP URIs, validate the metadata format + const response = await fetch(uri); + if (!response.ok) { + setUriError('Failed to fetch metadata'); + return false; + } + + const metadata = await response.json(); + if (!metadata.name || typeof metadata.name !== 'string') { + setUriError('Metadata must include a name property'); + return false; + } + if (!metadata.image || typeof metadata.image !== 'string') { + setUriError('Metadata must include an image property'); + return false; + } + if (!metadata.image.startsWith('ipfs://') && !metadata.image.startsWith('https://')) { + setUriError('Image URI must start with ipfs:// or https://'); + return false; + } + + return true; + } catch (error) { + setUriError(error instanceof Error ? error.message : 'Invalid metadata format'); + return false; + } finally { + setValidatingURI(false); + } + }; + + // Validate URI when it changes + useEffect(() => { + const timer = setTimeout(() => { + if (tokenURI) { + validateMetadata(tokenURI); + } else { + setUriError(null); + } + }, 500); + + return () => clearTimeout(timer); + }, [tokenURI]); // Check whitelist status when recipient changes useEffect(() => { const verifyWhitelist = async () => { if (isValidAddress(recipient)) { - const status = await checkWhitelist(recipient); - setIsWhitelisted(status); + try { + const status = await checkWhitelist(recipient); + setIsWhitelisted(status); + } catch (error) { + console.error('Failed to check whitelist status:', error); + setIsWhitelisted(false); + } } else { setIsWhitelisted(false); } }; - verifyWhitelist(); + + if (recipient) { + verifyWhitelist(); + } }, [recipient, checkWhitelist]); // Combined disable conditions const isDisabled = processing || !isValidAddress(recipient) || - !isValidURI(tokenURI); + validatingURI || + !!uriError || + !tokenURI; + + const handleSubmit = async () => { + if (await validateMetadata(tokenURI)) { + onSubmit(recipient, tokenURI); + } + }; return (
@@ -109,15 +188,23 @@ export default function MintForm({ value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="0x..." - className="w-full px-4 py-3 bg-gray-800 border-2 border-gray-700 rounded-xl - focus:border-orange-400 focus:ring-4 focus:ring-orange-400/20 - transition-all placeholder-gray-500 text-white font-mono text-sm" + className={`w-full px-4 py-3 bg-gray-800 border-2 rounded-xl + transition-all placeholder-gray-500 text-white font-mono text-sm + ${!isValidAddress(recipient) && recipient + ? 'border-red-500 focus:ring-red-500/20' + : 'border-gray-700 focus:border-orange-400 focus:ring-4 focus:ring-orange-400/20' + }`} /> - {!isValidAddress(recipient) && recipient !== '' && ( + {!isValidAddress(recipient) && recipient && (

⚠ Invalid BSC address

- )} + )} + {isValidAddress(recipient) && !isWhitelisted && ( +

+ ⚠ Address is not whitelisted +

+ )}
{/* Metadata URI Input */} @@ -128,13 +215,22 @@ export default function MintForm({ value={tokenURI} onChange={(e) => setTokenURI(e.target.value)} placeholder="ipfs://Qm... or https://" - className="w-full px-4 py-3 bg-gray-800 border-2 border-gray-700 rounded-xl - focus:border-orange-400 focus:ring-4 focus:ring-orange-400/20 - transition-all placeholder-gray-500 text-white text-sm" + className={`w-full px-4 py-3 bg-gray-800 border-2 rounded-xl + transition-all placeholder-gray-500 text-white text-sm + ${uriError + ? 'border-red-500 focus:ring-red-500/20' + : 'border-gray-700 focus:border-orange-400 focus:ring-4 focus:ring-orange-400/20' + }`} /> - {!isValidURI(tokenURI) && tokenURI !== '' && ( + {validatingURI && ( +

+ + Validating metadata... +

+ )} + {uriError && (

- ⚠ URI must start with ipfs:// or https:// + ⚠ {uriError}

)}
@@ -142,7 +238,7 @@ export default function MintForm({ {/* Mint Button */} + + + {/* Wallet Section */} +
+ {hasWallet && ( +
+ + {pathBalance} + + PATH +
+ )} + +
- - - - - ); -}; +
+ + -export default function NFTNavigation({ currentPath }: { currentPath?: string }) { - return ( -
- } - title="PATH NFT Marketplace" - description="Buy, sell, and create NFTs on the PATH token ecosystem. List your digital assets and trade with other users." - href="/NFT" - color="border-blue-500/30" - delay={0.1} - isActive={currentPath === '/NFT'} - image="/Img/Web3.webp" - /> - - } - title="NFT Collection Scanner" - description="Explore NFT collections across all EVM-based blockchains. Browse popular collections or connect your wallet to view your own NFTs." - href="/NFT/collection" - color="border-purple-500/30" - delay={0.2} - isActive={currentPath?.startsWith('/NFT/collection')} - image="/Img/Web3.webp" - /> + {/* Category Filters */} + {activeSection === 'marketplace' ? ( +
+ All NFTs + Art + Collectibles + Gaming + Membership + Virtual Land + Music + Photography + Sports + Utility +
+ ) : ( +
+ All Collections + Verified + Art + Gaming + PFP + Photography + Music + Metaverse +
+ )} + + {/* Feature Cards */} +
+ } + title="PATH NFT Marketplace" + description="Buy, sell, and create NFTs on the PATH token ecosystem. List your digital assets and trade with other users." + href="/NFT" + color="border-blue-500/30" + delay={0.1} + isActive={activeSection === 'marketplace'} + image="/Img/Web3.webp" + /> + + } + title="NFT Collection Scanner" + description="Explore NFT collections across all EVM-based blockchains. Browse popular collections or connect your wallet to view your own NFTs." + href="/NFT/collection" + color="border-purple-500/30" + delay={0.2} + isActive={activeSection === 'collections'} + image="/Img/Web3.webp" + /> +
-
+ {/* Tutorial Section */} +

New to NFTs?

diff --git a/components/NFT/PriceChart.tsx b/components/NFT/PriceChart.tsx new file mode 100644 index 0000000..8457fbd --- /dev/null +++ b/components/NFT/PriceChart.tsx @@ -0,0 +1,146 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; + +interface PricePoint { + date: string; + price: number; +} + +interface PriceChartProps { + data: PricePoint[]; + tokenId?: string; +} + +export default function PriceChart({ data, tokenId }: PriceChartProps) { + const [timeRange, setTimeRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h'); + const [chartData, setChartData] = useState([]); + + // Filter data based on selected time range + useEffect(() => { + if (!data || data.length === 0) { + setChartData([ + { date: '00:00', price: 100 }, + { date: '04:00', price: 120 }, + { date: '08:00', price: 90 }, + { date: '12:00', price: 150 }, + { date: '16:00', price: 180 }, + { date: '20:00', price: 200 }, + { date: '24:00', price: 160 }, + ]); // Placeholder data + return; + } + + const now = new Date(); + let filterDate = new Date(); + + switch (timeRange) { + case '1h': + filterDate.setHours(now.getHours() - 1); + break; + case '24h': + filterDate.setDate(now.getDate() - 1); + break; + case '7d': + filterDate.setDate(now.getDate() - 7); + break; + case '30d': + filterDate.setDate(now.getDate() - 30); + break; + } + + const filtered = data.filter((point) => { + const pointDate = new Date(point.date); + return pointDate >= filterDate; + }); + + setChartData(filtered); + }, [data, timeRange]); + + const formatYAxis = (value: number) => { + return `${value} PATH`; + }; + + // Calculate price change + const priceChange = chartData.length >= 2 + ? ((chartData[chartData.length - 1].price - chartData[0].price) / chartData[0].price) * 100 + : 0; + + const isPriceUp = priceChange >= 0; + + return ( + + +
+ + {tokenId ? `NFT #${tokenId} Price History` : 'Market Price Trends'} + + setTimeRange(v as any)}> + + 1H + 24H + 7D + 30D + + +
+
+ +
+
+

Current Price

+

+ {chartData.length > 0 ? chartData[chartData.length - 1].price : 0} PATH +

+
+
+ {priceChange.toFixed(2)}% +
+
+ +
+ + + + + + [`${value} PATH`, 'Price']} + /> + + + +
+
+
+ ); +} diff --git a/components/NFT/TradingHistory.tsx b/components/NFT/TradingHistory.tsx new file mode 100644 index 0000000..2c30d9d --- /dev/null +++ b/components/NFT/TradingHistory.tsx @@ -0,0 +1,273 @@ +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { ExternalLink, TrendingUp, ArrowUpRight, ShoppingCart, AlertTriangle } from 'lucide-react'; + +type EventType = 'Sale' | 'Transfer' | 'Mint' | 'List'; + +interface Trade { + id: string; + event: EventType; + tokenId: string; + from: string; + to: string; + price?: string; + timestamp: string; + txHash: string; +} + +interface TradingHistoryProps { + trades: Trade[]; + tokenId?: string; + loadMore?: () => void; + hasMore?: boolean; + loading?: boolean; +} + +type TabType = 'all' | 'sales' | 'transfers' | 'mints' | 'lists'; + +export default function TradingHistory({ + trades = [], + tokenId, + loadMore, + hasMore = false, + loading = false +}: TradingHistoryProps) { + const [activeTab, setActiveTab] = useState('all'); + const [error, setError] = useState(null); + + // Filter trades based on active tab + const filteredTrades = trades.filter(trade => { + if (activeTab === 'all') return true; + return trade.event.toLowerCase() === activeTab.slice(0, -1); + }); + + // Format addresses to be more readable + const formatAddress = (address: string) => { + return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; + }; + + // Format dates to be more readable + const formatDate = (dateStr: string) => { + try { + const date = new Date(dateStr); + return new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(date); + } catch (error) { + console.error('Invalid date format:', error); + return 'Invalid date'; + } + }; + + // Format price with proper decimal places + const formatPrice = (price?: string) => { + if (!price) return null; + try { + const numPrice = parseFloat(price); + return numPrice.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 4 + }); + } catch (error) { + console.error('Invalid price format:', error); + return price; + } + }; + + // Event badge styling with hover effects + const getEventBadge = (event: EventType) => { + const styles = { + Sale: 'bg-green-600 hover:bg-green-700', + Transfer: 'bg-blue-600 hover:bg-blue-700', + Mint: 'bg-purple-600 hover:bg-purple-700', + List: 'bg-orange-600 hover:bg-orange-700' + }; + + return ( + + {event} + + ); + }; + + const handleTabChange = (value: string) => { + setActiveTab(value as TabType); + }; + + return ( + + + + + {tokenId ? `NFT #${tokenId} Trading History` : 'Recent Transactions'} + + + + + + All + Sales + Transfers + Mints + Lists + + + + {error ? ( +
+ +

{error}

+
+ ) : ( + + + + Event + {!tokenId && Token ID} + Price + From + To + Date + + + + + {filteredTrades.map((trade) => ( + + + {getEventBadge(trade.event)} + + {!tokenId && ( + + + #{trade.tokenId} + + + + )} + + {trade.price ? ( +
+ {formatPrice(trade.price)} + PATH +
+ ) : ( + -- + )} +
+ +
+ + + {trade.from.substring(0, 2)} + + + {formatAddress(trade.from)} + +
+
+ +
+ + + {trade.to.substring(0, 2)} + + + {formatAddress(trade.to)} + +
+
+ + {formatDate(trade.timestamp)} + + + + + + +
+ ))} + + {loading && ( + + +
+
+ Loading transactions... +
+ + + )} + + {filteredTrades.length === 0 && !loading && ( + + +
+ +
+

No trading history available

+

+ {activeTab === 'all' + ? 'Be the first to make a transaction!' + : `No ${activeTab.slice(0, -1)} events found` + } +

+
+
+
+
+ )} + +
+ )} + + {hasMore && filteredTrades.length > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/components/NFT/WhitelistForm.tsx b/components/NFT/WhitelistForm.tsx index 85e8478..9cc88d9 100644 --- a/components/NFT/WhitelistForm.tsx +++ b/components/NFT/WhitelistForm.tsx @@ -1,32 +1,82 @@ -// components/NFT/WhitelistForm.tsx -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { utils } from 'ethers'; -import { Loader2, CheckCircle, XCircle } from 'lucide-react'; +import { Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; +import { toast } from '@/hooks/use-toast'; interface WhitelistFormProps { onSubmit: (address: string, status: boolean) => Promise; isOwner: boolean; } +interface Transaction { + address: string; + status: boolean; + timestamp: number; +} + export default function WhitelistForm({ onSubmit, isOwner }: WhitelistFormProps) { const [address, setAddress] = useState(''); const [processing, setProcessing] = useState(false); const [status, setStatus] = useState<'success' | 'error' | null>(null); + const [errorMessage, setErrorMessage] = useState(''); + const [previousTransactions, setPreviousTransactions] = useState([]); + + // Load previous transactions from localStorage + useEffect(() => { + const saved = localStorage.getItem('whitelistTransactions'); + if (saved) { + try { + const parsed = JSON.parse(saved); + setPreviousTransactions(parsed); + } catch (error) { + console.error('Failed to parse saved transactions:', error); + } + } + }, []); + + // Save transactions to localStorage + const saveTransaction = (address: string, status: boolean) => { + const transaction = { + address, + status, + timestamp: Date.now() + }; + const newTransactions = [transaction, ...previousTransactions].slice(0, 10); // Keep last 10 + setPreviousTransactions(newTransactions); + localStorage.setItem('whitelistTransactions', JSON.stringify(newTransactions)); + }; const isValidAddress = (addr: string) => utils.isAddress(addr); - const handleSubmit = async (status: boolean) => { - if (!isValidAddress(address)) return; + const handleSubmit = async (newStatus: boolean) => { + if (!isValidAddress(address)) { + setErrorMessage('Invalid address format'); + return; + } setProcessing(true); setStatus(null); + setErrorMessage(''); + try { - await onSubmit(address, status); + await onSubmit(address, newStatus); setStatus('success'); + saveTransaction(address, newStatus); + toast({ + title: 'Success', + description: `Address ${newStatus ? 'added to' : 'removed from'} whitelist`, + variant: 'default' + }); setAddress(''); } catch (error) { console.error(error); setStatus('error'); + setErrorMessage(error instanceof Error ? error.message : 'Operation failed'); + toast({ + title: 'Error', + description: error instanceof Error ? error.message : 'Operation failed', + variant: 'destructive' + }); } finally { setProcessing(false); } @@ -49,12 +99,19 @@ export default function WhitelistForm({ onSubmit, isOwner }: WhitelistFormProps) value={address} onChange={(e) => setAddress(e.target.value)} placeholder="0x..." - className="w-full px-4 py-3 bg-gray-800 border-2 border-gray-700 rounded-xl - focus:border-orange-400 focus:ring-4 focus:ring-orange-400/20 - transition-all placeholder-gray-500 text-white font-mono text-sm" + className={`w-full px-4 py-3 bg-gray-800 border-2 rounded-xl + transition-all placeholder-gray-500 text-white font-mono text-sm + ${ + errorMessage + ? 'border-red-500 focus:ring-red-500/20' + : 'border-gray-700 focus:border-orange-400 focus:ring-4 focus:ring-orange-400/20' + }`} /> {!isValidAddress(address) && address !== '' && ( -

⚠ Invalid address format

+

+ + Invalid address format +

)}
@@ -62,29 +119,80 @@ export default function WhitelistForm({ onSubmit, isOwner }: WhitelistFormProps)
{status === 'success' && ( -

✓ Operation successful!

+

+ + Operation successful! +

)} {status === 'error' && ( -

⚠ Operation failed

+

+ + {errorMessage} +

+ )} + + {/* Recent Transactions */} + {previousTransactions.length > 0 && ( +
+

Recent Transactions

+
+ {previousTransactions.map((tx, index) => ( +
+
+ {tx.status ? ( + + ) : ( + + )} + + {tx.address.slice(0, 6)}...{tx.address.slice(-4)} + +
+ + {new Date(tx.timestamp).toLocaleString()} + +
+ ))} +
+
)}
diff --git a/lib/api/alchemyNFTApi.ts b/lib/api/alchemyNFTApi.ts index fb63ff7..7c7f6d5 100644 --- a/lib/api/alchemyNFTApi.ts +++ b/lib/api/alchemyNFTApi.ts @@ -1,4 +1,3 @@ - import { toast } from "sonner"; const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || 'demo'; @@ -221,57 +220,122 @@ export async function fetchCollectionNFTs( } } -export async function fetchPopularCollections(chainId: string = '0x1'): Promise { - try { - // In a production app, you would fetch this from a backend API - // For now, we'll use a mock response - return [ - { - id: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', - name: 'Bored Ape Yacht Club', - imageUrl: 'https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format', - floorPrice: 30.5, - totalSupply: 10000, - }, - { - id: '0x60e4d786628fea6478f785a6d7e704777c86a7c6', - name: 'Mutant Ape Yacht Club', - imageUrl: 'https://i.seadn.io/gae/lHexKRMpw-aoSyB1WdFBff5yfANLReFxHzt1DOj_sg7mS14yARpuvYcUtsyyx-Nkpk6WTcUPFoG53VnLJezYi8hAs0OxNZwlw6Y-dmI?w=500&auto=format', - floorPrice: 12.2, - totalSupply: 20000, - }, - { - id: '0xed5af388653567af2f388e6224dc7c4b3241c544', - name: 'Azuki', - imageUrl: 'https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format', - floorPrice: 8.9, - totalSupply: 10000, - }, - { - id: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb', - name: 'CryptoPunks', - imageUrl: 'https://i.seadn.io/gae/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE?w=500&auto=format', - floorPrice: 50.2, - totalSupply: 10000, - }, - { - id: '0x8a90cab2b38dba80c64b7734e58ee1db38b8992e', - name: 'Doodles', - imageUrl: 'https://i.seadn.io/gae/7B0qai02OdHA8P_EOVK672qUliyjQdQDGNrACxs7WnTgZAkJa_wWURnIFKeOh5VTf8cfTqW3wQpozGedaC9mteKphEOtztls02RlWQ?w=500&auto=format', - floorPrice: 3.8, - totalSupply: 10000, - }, - { - id: '0x1a92f7381b9f03921564a437210bb9396471050c', - name: 'Cool Cats', - imageUrl: 'https://i.seadn.io/gae/LIov33kogXOK4XZd2ESj29sqm_Hww5JSdO7AFn5wjt8xgnJJ0UpNV9yITqxra3s_LMEW1AnnrgOVB_hDpjJRA1uF4skI5Sdi_9rULi8?w=500&auto=format', - floorPrice: 2.1, - totalSupply: 9999, - }, - ]; - } catch (error) { - console.error('Error fetching popular collections:', error); - toast.error("Failed to load popular collections"); - return []; +// Mocked API service for NFT data +// In a real application, this would connect to Alchemy or another provider +export async function fetchPopularCollections(chainId: string): Promise { + // In a production app, this would fetch from Alchemy API + // For this demo, we'll return mock data + return [ + { + id: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + name: "Bored Ape Yacht Club", + totalSupply: 10000, + floorPrice: "30.5", + imageUrl: "https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?auto=format&dpr=1&w=1000" + }, + { + id: "0x60e4d786628fea6478f785a6d7e704777c86a7c6", + name: "Mutant Ape Yacht Club", + totalSupply: 19423, + floorPrice: "10.2", + imageUrl: "https://i.seadn.io/gae/lHexKRMpw-aoSyB1WdFBff5yfANLReFxHzt1DOj_sg7mS14yARpuvYcUtsyyx-Nkpk6WTcUPF6rLh2D4Xw?auto=format&dpr=1&w=1000" + }, + { + id: "0xed5af388653567af2f388e6224dc7c4b3241c544", + name: "Azuki", + totalSupply: 10000, + floorPrice: "8.75", + imageUrl: "https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?auto=format&dpr=1&w=1000" + }, + { + id: "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", + name: "CryptoPunks", + totalSupply: 10000, + floorPrice: "54.95", + imageUrl: "https://i.seadn.io/gae/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE?auto=format&dpr=1&w=1000" + }, + { + id: "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + name: "Doodles", + totalSupply: 10000, + floorPrice: "5.25", + imageUrl: "https://i.seadn.io/gae/7B0qai02OdHA8P_EOVK672qUliyjQdQDGNrACxs7WnTgZAkJa_wWURnIFKeOh5VTf8cfTqW3wQpozGedaC9mteKphEOtztls02RlWQ?auto=format&dpr=1&w=1000" + }, + { + id: "0xdf5d4038723f6605a3ecd7776ffe25f3b1be39a0", + name: "PATH NFT Collection", + totalSupply: 1000, + floorPrice: "0.5", + imageUrl: "/images/path-token.png" + } + ]; +} + +// Function to fetch marketplace trading history +export async function fetchTradeHistory(tokenId?: string): Promise { + // This would normally connect to a blockchain indexer service + // For now, we'll return mock data + return [ + { + id: '1', + event: 'Sale', + tokenId: tokenId || '123', + from: '0x1234567890abcdef1234567890abcdef12345678', + to: '0xabcdef1234567890abcdef1234567890abcdef12', + price: '120.5', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), // 2 hours ago + txHash: '0xabc123def456' + }, + { + id: '2', + event: 'Transfer', + tokenId: tokenId || '123', + from: '0xabcdef1234567890abcdef1234567890abcdef12', + to: '0x9876543210abcdef1234567890abcdef12345678', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), // 1 day ago + txHash: '0xdef456abc789' + }, + { + id: '3', + event: 'Mint', + tokenId: tokenId || '123', + from: '0x0000000000000000000000000000000000000000', + to: '0x1234567890abcdef1234567890abcdef12345678', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(), // 3 days ago + txHash: '0x789abc123def' + }, + { + id: '4', + event: 'List', + tokenId: tokenId || '123', + from: '0x1234567890abcdef1234567890abcdef12345678', + to: '0x0000000000000000000000000000000000000000', + price: '100', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(), // 2 days ago + txHash: '0x456def789abc' + } + ]; +} + +// Function to fetch price history data for charts +export async function fetchPriceHistory(tokenId?: string): Promise { + // This would normally fetch real historical price data + // For now, generate some mock data + const now = Date.now(); + const data = []; + + // Generate 30 days of price data + for (let i = 30; i >= 0; i--) { + const date = new Date(now - 1000 * 60 * 60 * 24 * i); + const basePrice = tokenId ? 100 : 120; // Different base for collection vs single NFT + const randomFactor = 0.3 * Math.sin(i / 2) + 0.2 * Math.cos(i); + const volatility = 0.1; + + data.push({ + date: date.toISOString().split('T')[0], + price: basePrice * (1 + randomFactor + volatility * (Math.random() - 0.5)) + }); } + + return data; } diff --git a/next.config.js b/next.config.js index 54a91cc..e45acde 100644 --- a/next.config.js +++ b/next.config.js @@ -260,7 +260,7 @@ const nextConfig = { // Add important settings for Vercel deployment experimental: { // Allow more time for API routes that make external calls - serverComponentsExternalPackages: [], + serverExternalPackages: [], }, // Add extra security headers async headers() { From fde3c88d07216c9218abd975dcf203d429ba3cb0 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:15:26 +0700 Subject: [PATCH 081/107] Add wallet connection button to NFT layout with account display --- app/NFT/layout.tsx | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/app/NFT/layout.tsx b/app/NFT/layout.tsx index 7bae892..ef9b177 100644 --- a/app/NFT/layout.tsx +++ b/app/NFT/layout.tsx @@ -4,12 +4,15 @@ import React, { useState, useEffect } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import Link from 'next/link'; import { motion } from 'framer-motion'; -import { LayoutGrid, Layers, Bookmark, Activity, ChevronRight, Info } from 'lucide-react'; +import { LayoutGrid, Layers, Bookmark, Activity, ChevronRight, Info, Wallet } from 'lucide-react'; +import { Button } from '@/components/ui/button'; // Add Button import +import { useWallet } from '@/components/Faucet/walletcontext'; // Add wallet context import export default function NFTLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const router = useRouter(); const [mounted, setMounted] = useState(false); + const { account, connectWallet } = useWallet(); // Add wallet context hooks // Determine active section based on URL const isMarketplace = pathname === '/NFT'; @@ -24,9 +27,9 @@ export default function NFTLayout({ children }: { children: React.ReactNode }) { return (
- {/* Navigation Tabs */} -
-
+ {/* Navigation Tabs with Wallet Button */} +
+
+ + {/* Connect Wallet Button */} +
{/* Breadcrumb Navigation */} From bccc77e6331afd82a5f73c7ef950f33ce3874817 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:24:31 +0700 Subject: [PATCH 082/107] final --- app/NFT/layout.tsx | 7 +++++-- components/NFT/PriceChart.tsx | 2 +- next.config.js | 5 +---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/NFT/layout.tsx b/app/NFT/layout.tsx index ef9b177..165487b 100644 --- a/app/NFT/layout.tsx +++ b/app/NFT/layout.tsx @@ -71,8 +71,11 @@ export default function NFTLayout({ children }: { children: React.ReactNode }) {
{/* Connect Wallet Button */} - -
- setAddress(e.target.value)} - className="pl-10 pr-10 py-2 h-9 w-64 text-sm transition-all duration-200 focus:border-amber-500 rounded-[5px]" - /> - {address.length > 0 && ( - - )} -
- - */} + {currentUser ? (
@@ -266,6 +235,12 @@ const Header = () => { > Setting +
)}
@@ -335,43 +310,7 @@ const Header = () => { > Search - {/*
- - setAddress(e.target.value)} - className="pl-10 pr-10 py-2 w-full text-black transition-all duration-200 focus:border-amber-500" - /> - {address.length > 0 && ( - - )} - -
*/} + {currentUser ? (
@@ -390,6 +329,12 @@ const Header = () => { > Setting +
) : ( diff --git a/components/portfolio/ActivityTable.tsx b/components/portfolio/ActivityTable.tsx new file mode 100644 index 0000000..690fe9f --- /dev/null +++ b/components/portfolio/ActivityTable.tsx @@ -0,0 +1,150 @@ +"use client"; +import React, { useState } from "react"; +import { ArrowDownUp, ArrowDown, ArrowUp, ExternalLink, ChevronLeft, ChevronRight } from "lucide-react"; +import { formatDistanceToNow } from "date-fns"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ethers } from "ethers"; + +interface Transaction { + hash: string; + timeStamp: string; + from: string; + to: string; + value: string; + isError: string; +} + +interface ActivityTableProps { + transactions: Transaction[]; + walletAddress: string; + isLoading: boolean; +} + +const ActivityTable: React.FC = ({ transactions, walletAddress, isLoading }) => { + const [currentPage, setCurrentPage] = useState(1); + const txPerPage = 5; + + const indexOfLastTx = currentPage * txPerPage; + const indexOfFirstTx = indexOfLastTx - txPerPage; + const currentTxs = transactions.slice(indexOfFirstTx, indexOfLastTx); + const totalPages = Math.ceil(transactions.length / txPerPage); + + const nextPage = () => currentPage < totalPages && setCurrentPage(currentPage + 1); + const prevPage = () => currentPage > 1 && setCurrentPage(currentPage - 1); + + const formatAddress = (address: string) => + address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "Unknown"; + + const formatEthValue = (value: string) => + ethers.utils.formatEther(value || "0").substring(0, 6); + + const getTransactionType = (tx: Transaction) => + tx.to.toLowerCase() === walletAddress.toLowerCase() ? "incoming" : "outgoing"; + + return ( +
+
+
+
+ +
+

Activity

+
+ {transactions.length > 0 && ( +
+ {indexOfFirstTx + 1}-{Math.min(indexOfLastTx, transactions.length)} of {transactions.length} +
+ )} +
+ +
+ {isLoading ? ( + [...Array(5)].map((_, i) => ) + ) : transactions.length > 0 ? ( + currentTxs.map((tx, index) => ( +
+
+
+
+ {tx.isError === "1" ? ( + "✕" + ) : getTransactionType(tx) === "incoming" ? ( + + ) : ( + + )} +
+
+ {tx.isError === "1" + ? "Failed" + : getTransactionType(tx) === "incoming" + ? "Received" + : "Sent"} +
+
+
+ {formatDistanceToNow(parseInt(tx.timeStamp) * 1000, { addSuffix: true })} +
+
+
+

From: {formatAddress(tx.from)}

+

To: {formatAddress(tx.to)}

+

Value: {formatEthValue(tx.value)} ETH

+ + View on Etherscan + +
+
+ )) + ) : ( +
+ +

No activity found

+
+ )} +
+ + {transactions.length > txPerPage && ( +
+ + +
+ )} +
+ ); +}; + +export default ActivityTable; \ No newline at end of file diff --git a/components/portfolio/Allocation.tsx b/components/portfolio/Allocation.tsx new file mode 100644 index 0000000..d1cbdd7 --- /dev/null +++ b/components/portfolio/Allocation.tsx @@ -0,0 +1,143 @@ +"use client"; +import React from "react"; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"; +import { CircleDollarSign, PieChart as PieChartIcon } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface Token { + name: string; + symbol: string; + balance: string; + tokenAddress: string; + decimals: number; + value: number; +} + +interface AllocationChartProps { + tokens: Token[]; + ethBalance: string; + isLoading: boolean; +} + +const AllocationChart: React.FC = ({ tokens, ethBalance, isLoading }) => { + const calculateAllocation = () => { + const ethValue = parseFloat(ethBalance || "0"); + + if (tokens.length === 0 && ethValue <= 0) { + return []; + } + + // Danh sách màu cố định, đảm bảo mỗi coin có màu riêng biệt + const colors = [ + "#f6b355", // ETH + "#FF6B6B", // Token 1 + "#48dbfb", // Token 2 + "#1dd1a1", // Token 3 + "#feca57", // Token 4 + "#ff9ff3", // Token 5 + "#54a0ff", // Token 6 + ]; + + const assets = ethValue > 0 ? [{ name: "ETH", value: ethValue, color: colors[0] }] : []; + const uniqueTokens = new Map(); + + tokens.forEach((token, index) => { + if (uniqueTokens.has(token.symbol) || token.value <= 0) return; + + uniqueTokens.set(token.symbol, true); + const colorIndex = assets.length; // Đảm bảo màu không trùng với ETH hoặc token khác + assets.push({ + name: token.symbol, + value: token.value, + color: colors[colorIndex % colors.length], // Lấy màu từ danh sách + }); + }); + + const topAssets = assets.sort((a, b) => b.value - a.value).slice(0, 7); + const totalValue = topAssets.reduce((sum, asset) => sum + asset.value, 0); + return totalValue > 0 ? topAssets : []; + }; + + const data = calculateAllocation(); + + const formatTooltipValue = (value: number) => `${value.toFixed(4)} ETH`; + + const renderLegend = (props: any) => { + const { payload } = props; + return ( +
    + {payload.map((entry: any, index: number) => ( +
  • + + {entry.value} +
  • + ))} +
+ ); + }; + + return ( +
+
+
+ +
+

Asset Allocation

+
+ +
+ {isLoading ? ( +
+ +
+ ) : data.length > 0 ? ( + + + + {data.map((entry, index) => ( + + ))} + + + + + + ) : ( +
+ +

No assets found to display allocation

+
+ )} +
+
+ ); +}; + +export default AllocationChart; \ No newline at end of file diff --git a/components/portfolio/BalanceCard.tsx b/components/portfolio/BalanceCard.tsx new file mode 100644 index 0000000..c56d5e3 --- /dev/null +++ b/components/portfolio/BalanceCard.tsx @@ -0,0 +1,90 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { ArrowUpRight, Wallet } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface BalanceCardProps { + balance: string; + isLoading: boolean; +} + +const BalanceCard: React.FC = ({ balance, isLoading }) => { + const [currentTime, setCurrentTime] = useState(""); + + useEffect(() => { + setCurrentTime(new Date().toLocaleTimeString()); + }, [balance]); + + const formattedBalance = isLoading || !balance ? "0.0000" : parseFloat(balance).toFixed(4); + + return ( +
+ {/* Hiệu ứng hover border */} +
{ + e.currentTarget.style.borderColor = "#f6b355"; + e.currentTarget.style.opacity = "0.8"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = "transparent"; + e.currentTarget.style.opacity = "0"; + }} + /> + +
+
+
+ +
+

+ ETH Balance +

+
+ + Etherscan + + +
+ +
+ {isLoading ? ( + + ) : ( +
+ + {formattedBalance} + + ETH +
+ )} +
+ {isLoading ? ( + + ) : ( + Last updated: {currentTime || "Just now"} + )} +
+
+
+ ); +}; + +export default BalanceCard; \ No newline at end of file diff --git a/components/portfolio/HistoryCard.tsx b/components/portfolio/HistoryCard.tsx new file mode 100644 index 0000000..f50feb4 --- /dev/null +++ b/components/portfolio/HistoryCard.tsx @@ -0,0 +1,104 @@ +"use client"; +import React, { useState } from "react"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; +import { AreaChart, Clock } from "lucide-react"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ethers } from "ethers"; + +interface Transaction { + hash: string; + timeStamp: string; + value: string; // Chuỗi thập phân từ API (wei) +} + +interface HistoryChartProps { + transactions: Transaction[]; + isLoading: boolean; +} + +const HistoryChart: React.FC = ({ transactions, isLoading }) => { + const [selectedPeriod, setSelectedPeriod] = useState("all"); + + const processChartData = (txs: Transaction[], period: string) => { + if (!txs || txs.length === 0) return []; + + const sortedTxs = [...txs].sort((a, b) => parseInt(a.timeStamp) - parseInt(b.timeStamp)); + let filteredTxs = sortedTxs; + const now = Math.floor(Date.now() / 1000); + + if (period === "1d") { + filteredTxs = sortedTxs.filter((tx) => now - parseInt(tx.timeStamp) < 86400); + } else if (period === "7d") { + filteredTxs = sortedTxs.filter((tx) => now - parseInt(tx.timeStamp) < 604800); + } else if (period === "30d") { + filteredTxs = sortedTxs.filter((tx) => now - parseInt(tx.timeStamp) < 2592000); + } + + const groupedData: Record = {}; + + filteredTxs.forEach((tx) => { + const timestampMs = parseInt(tx.timeStamp) * 1000; + if (isNaN(timestampMs)) return; + + const date = new Date(timestampMs).toLocaleDateString(); + // Chuyển đổi giá trị từ wei sang ETH + const valueInWei = tx.value || "0"; // Đảm bảo không có undefined + const valueInEth = parseFloat(ethers.utils.formatEther(valueInWei)); + + if (groupedData[date]) { + groupedData[date].value += valueInEth; + } else { + groupedData[date] = { date, value: valueInEth }; + } + }); + + return Object.values(groupedData); + }; + + const chartData = processChartData(transactions, selectedPeriod); + + return ( +
+
+
+
+ +
+

Transaction History

+
+ + + 1D + 7D + 30D + All + + +
+ +
+ {isLoading ? ( + + ) : chartData.length > 0 ? ( + + + + + + `${value.toFixed(4)} ETH`} /> + + + + ) : ( +
+ +

No transaction history available

+
+ )} +
+
+ ); +}; + +export default HistoryChart; \ No newline at end of file diff --git a/components/portfolio/NFTsCard.tsx b/components/portfolio/NFTsCard.tsx new file mode 100644 index 0000000..76f8dc8 --- /dev/null +++ b/components/portfolio/NFTsCard.tsx @@ -0,0 +1,147 @@ +"use client"; +import React, { useState } from "react"; +import { Image, ChevronRight, ChevronLeft } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; + +interface NFT { + name: string; + collectionName: string; + description: string; + tokenId: string; + contract: string; + imageUrl?: string; +} + +interface NFTsCardProps { + nfts: NFT[]; + isLoading: boolean; +} + +const NFTsCard: React.FC = ({ nfts, isLoading }) => { + const [currentPage, setCurrentPage] = useState(1); + const nftsPerPage = 2; + + const indexOfLastNFT = currentPage * nftsPerPage; + const indexOfFirstNFT = indexOfLastNFT - nftsPerPage; + const currentNFTs = nfts.slice(indexOfFirstNFT, indexOfLastNFT); + const totalPages = Math.ceil(nfts.length / nftsPerPage); + + const nextPage = () => { + if (currentPage < totalPages) setCurrentPage(currentPage + 1); + }; + + const prevPage = () => { + if (currentPage > 1) setCurrentPage(currentPage - 1); + }; + + return ( +
+
+
+
+ +
+

NFT Collection

+
+ {nfts.length > 0 && ( +
+ {indexOfFirstNFT + 1}-{Math.min(indexOfLastNFT, nfts.length)} of {nfts.length} +
+ )} +
+ +
+ {isLoading ? ( + [...Array(2)].map((_, i) => ( + + )) + ) : nfts.length > 0 ? ( + currentNFTs.map((nft, index) => ( +
+
+ {nft.imageUrl ? ( + {nft.name} { + const target = e.target as HTMLImageElement | null; + if (target) { + target.style.display = "none"; + const nextSibling = target.nextSibling as HTMLElement | null; + if (nextSibling) { + nextSibling.style.display = "block"; + } + } + }} + + /> + ) : ( +
+ +
+ )} +
+
+
+

+ {nft.name || `#${nft.tokenId}`} +

+

{nft.collectionName || "Unknown Collection"}

+

+ {nft.description || "No description available"} +

+

+ Token ID: {nft.tokenId.length > 8 ? `${nft.tokenId.substring(0, 8)}...` : nft.tokenId} +

+
+
+ )) + ) : ( +
+ +

No NFTs found for this wallet

+
+ )} +
+ + {nfts.length > nftsPerPage && ( +
+ + +
+ )} +
+ ); +}; + +export default NFTsCard; \ No newline at end of file diff --git a/components/portfolio/TokenCard.tsx b/components/portfolio/TokenCard.tsx new file mode 100644 index 0000000..f58d5be --- /dev/null +++ b/components/portfolio/TokenCard.tsx @@ -0,0 +1,121 @@ +"use client"; +import React, { useState } from "react"; +import { Coins, ChevronLeft, ChevronRight } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; + +interface Token { + name: string; + symbol: string; + balance: string; // Chuỗi thập phân đã định dạng + tokenAddress: string; + decimals: number; + logo?: string; + value: number; +} + +interface TokensCardProps { + tokens: Token[]; + isLoading: boolean; +} + +const TokensCard: React.FC = ({ tokens, isLoading }) => { + const [currentPage, setCurrentPage] = useState(1); + const tokensPerPage = 3; // Giới hạn 5 token mỗi trang + + // Tính toán chỉ số token hiển thị trên trang hiện tại + const indexOfLastToken = currentPage * tokensPerPage; + const indexOfFirstToken = indexOfLastToken - tokensPerPage; + const currentTokens = tokens.slice(indexOfFirstToken, indexOfLastToken); + const totalPages = Math.ceil(tokens.length / tokensPerPage); + + // Hàm chuyển trang + const nextPage = () => { + if (currentPage < totalPages) setCurrentPage(currentPage + 1); + }; + + const prevPage = () => { + if (currentPage > 1) setCurrentPage(currentPage - 1); + }; + + return ( +
+
+
+ +
+

Tokens

+
+ +
+ {isLoading ? ( + [...Array(5)].map((_, i) => ( + + )) + ) : tokens.length > 0 ? ( + currentTokens.map((token, index) => ( +
+
+ {token.logo ? ( + {token.symbol} (e.currentTarget.src = "/placeholder-token.png")} + /> + ) : ( + + )} +
+
+
{token.name}
+
+ {parseFloat(token.balance).toFixed(4)} {token.symbol} +
+
+
+ )) + ) : ( +
+ +

No tokens found

+
+ )} +
+ + {/* Nút chuyển trang */} + {tokens.length > tokensPerPage && ( +
+ +
+ Page {currentPage} of {totalPages} +
+ +
+ )} +
+ ); +}; + +export default TokensCard; \ No newline at end of file diff --git a/components/portfolio/WalletSearch.tsx b/components/portfolio/WalletSearch.tsx new file mode 100644 index 0000000..4a7c9d5 --- /dev/null +++ b/components/portfolio/WalletSearch.tsx @@ -0,0 +1,93 @@ +"use client"; +import React, { useState } from 'react'; +import { Search, ArrowRight } from 'lucide-react'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from '@/components/ui/use-toast'; + +interface WalletSearchProps { + onSearch: (address: string) => void; + isLoading: boolean; +} + +const WalletSearch: React.FC = ({ onSearch, isLoading }) => { + const [address, setAddress] = useState(''); + const [isFocused, setIsFocused] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!address.trim()) { + toast({ + title: "Invalid Address", + description: "Please enter a valid wallet address", + variant: "destructive", + }); + return; + } + + if (address.trim().length < 26) { + toast({ + title: "Address too short", + description: "Please enter a complete wallet address", + variant: "destructive", + }); + return; + } + + onSearch(address.trim()); + }; + + return ( +
+
+

+ Wallet Portfolio Scanner +

+

+ Enter an Ethereum wallet address to view its assets and activity in real-time +

+
+ +
+
+
+ +
+ setAddress(e.target.value)} + placeholder="Enter wallet address (0x...)" + className="pl-10 pr-20 py-6 backdrop-blur-xl bg-shark-800/70 border border-amber/20 text-gray-200 placeholder:text-gray-500 focus:border-amber focus:ring-amber w-full" + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + style={{ + boxShadow: isFocused ? '0 0 10px 2px rgba(246, 179, 85, 0.4)' : 'none' + }} + /> + +
+
+
+ ); +}; + +export default WalletSearch; diff --git a/components/portfolio_service/alchemyService.tsx b/components/portfolio_service/alchemyService.tsx new file mode 100644 index 0000000..8e27b92 --- /dev/null +++ b/components/portfolio_service/alchemyService.tsx @@ -0,0 +1,235 @@ +"use client"; +import { ethers } from "ethers"; + +const ALCHEMY_API_KEY = "vHX215j9gH01Qc94rX2eEAsLeYohIu9X"; +const API_BASE_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; + +export interface WalletData { + balance: string; + tokens: Token[]; + nfts: NFT[]; + transactions: Transaction[]; +} + +export interface Token { + name: string; + symbol: string; + balance: string; + tokenAddress: string; + decimals: number; + logo?: string; + value: number; +} + +export interface NFT { + name: string; + collectionName: string; + description: string; + tokenId: string; + contract: string; + imageUrl?: string; +} + +export interface Transaction { + hash: string; + timeStamp: string; + from: string; + to: string; + value: string; + isError: string; +} + +const fetchAlchemyApi = async (endpoint: string, params: Record) => { + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: params.method, + params: params.params, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + if (data.error) { + console.error("Alchemy API error:", data.error); + throw new Error(`API error: ${data.error.message || "Unknown error"}`); + } + + return data.result; + } catch (error) { + console.error("Fetch Alchemy API failed:", error); + throw error; // Ném lỗi để hàm gọi xử lý + } +}; + +export const getWalletBalance = async (address: string): Promise => { + if (!ethers.utils.isAddress(address)) { + console.warn("Invalid Ethereum address:", address); + return "0"; + } + try { + const result = await fetchAlchemyApi("", { + method: "eth_getBalance", + params: [address, "latest"], + }); + const balance = ethers.utils.formatEther(result || "0"); + console.log(`Balance for ${address}: ${balance} ETH`); + return balance; + } catch (error) { + console.error("Error fetching balance:", error); + return "0"; + } +}; + +export const getWalletTokens = async (address: string): Promise => { + if (!ethers.utils.isAddress(address)) { + console.warn("Invalid Ethereum address:", address); + return []; + } + try { + const tokenBalances = await fetchAlchemyApi("", { + method: "alchemy_getTokenBalances", + params: [address, "DEFAULT_TOKENS"], // Lấy danh sách token mặc định + }); + + if (!tokenBalances?.tokenBalances) return []; + + const tokens = await Promise.all( + tokenBalances.tokenBalances + .filter((token: any) => token.tokenBalance && token.tokenBalance !== "0x0") + .map(async (token: any) => { + const metadata = await fetchAlchemyApi("", { + method: "alchemy_getTokenMetadata", + params: [token.contractAddress], + }); + + const balanceInWei = ethers.BigNumber.from(token.tokenBalance); + const decimals = metadata.decimals || 18; + const balance = ethers.utils.formatUnits(balanceInWei, decimals); + + return { + name: metadata.name || "Unknown Token", + symbol: metadata.symbol || "UNK", + balance, + tokenAddress: token.contractAddress, + decimals, + logo: metadata.logo || undefined, + value: parseFloat(balance) * 0.001, // Giá trị giả lập + }; + }) + ); + console.log(`Tokens for ${address}:`, tokens); + return tokens; + } catch (error) { + console.error("Error fetching tokens:", error); + return []; + } +}; + +export const getWalletNFTs = async (address: string): Promise => { + if (!ethers.utils.isAddress(address)) { + console.warn("Invalid Ethereum address:", address); + return []; + } + try { + const nfts = await fetchAlchemyApi("", { + method: "alchemy_getNFTs", + params: [{ owner: address, withMetadata: true }], // Đúng cú pháp cho alchemy_getNFTs + }); + + if (!nfts?.ownedNfts) return []; + + const nftList = nfts.ownedNfts.map((nft: any) => ({ + name: nft.metadata?.name || nft.title || `NFT #${nft.id.tokenId}`, + collectionName: nft.contract?.name || "Unknown Collection", + description: nft.metadata?.description || nft.description || "", + tokenId: nft.id.tokenId, + contract: nft.contract.address, + imageUrl: nft.metadata?.image || nft.media?.[0]?.gateway || nft.image?.thumbnailUrl, + })); + console.log(`NFTs for ${address}:`, nftList); + return nftList; + } catch (error) { + console.error("Error fetching NFTs:", error); + return []; + } +}; + +export const getWalletTransactions = async (address: string): Promise => { + if (!ethers.utils.isAddress(address)) { + console.warn("Invalid Ethereum address:", address); + return []; + } + try { + const transfersFrom = await fetchAlchemyApi("", { + method: "alchemy_getAssetTransfers", + params: [ + { + fromBlock: "0x0", + toBlock: "latest", + fromAddress: address, + category: ["external"], // Chỉ lấy giao dịch ETH + withMetadata: true, + }, + ], + }); + + const transfersTo = await fetchAlchemyApi("", { + method: "alchemy_getAssetTransfers", + params: [ + { + fromBlock: "0x0", + toBlock: "latest", + toAddress: address, + category: ["external"], + withMetadata: true, + }, + ], + }); + + const allTransfers = [...(transfersFrom?.transfers || []), ...(transfersTo?.transfers || [])]; + if (!allTransfers.length) { + console.log(`No transactions found for ${address}`); + return []; + } + + const txs = allTransfers.map((tx: any) => ({ + hash: tx.hash || "", + timeStamp: + tx.metadata?.blockTimestamp + ? Math.floor(new Date(tx.metadata.blockTimestamp).getTime() / 1000).toString() + : "0", + from: tx.from || "", + to: tx.to || "", + value: tx.value ? ethers.utils.parseEther(tx.value.toString()).toString() : "0", + isError: "0", // Alchemy không cung cấp thông tin lỗi, mặc định là 0 + })); + console.log(`Transactions for ${address}:`, txs); + return txs; + } catch (error) { + console.error("Error fetching transactions:", error); + return []; + } +}; + +export const getWalletData = async (address: string): Promise => { + try { + const [balance, tokens, nfts, transactions] = await Promise.all([ + getWalletBalance(address), + getWalletTokens(address), + getWalletNFTs(address), + getWalletTransactions(address), + ]); + return { balance, tokens, nfts, transactions }; + } catch (error) { + console.error("Error fetching wallet data:", error); + return { balance: "0", tokens: [], nfts: [], transactions: [] }; + } +}; \ No newline at end of file diff --git a/next.config.js b/next.config.js index e33f880..54a91cc 100644 --- a/next.config.js +++ b/next.config.js @@ -258,7 +258,10 @@ const nextConfig = { return config; }, // Add important settings for Vercel deployment - experimental: {}, + experimental: { + // Allow more time for API routes that make external calls + serverComponentsExternalPackages: [], + }, // Add extra security headers async headers() { return [ From be52661686c9861a283ecc65ada8d242d75d092d Mon Sep 17 00:00:00 2001 From: HungPhan-0612 <163500971+HungPhan-0612@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:17:59 +0700 Subject: [PATCH 086/107] Update Header and Portfolio components for responsive design and provider selection --- components/Header.tsx | 4 ++-- components/search/Portfolio.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/Header.tsx b/components/Header.tsx index 05734b3..6e65ae1 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -179,7 +179,7 @@ const Header = () => {
-
@@ -304,9 +304,9 @@ export default function Portfolio() { onChange={(e) => setProvider(e.target.value as "moralis" | "alchemy" | "combined")} className="bg-gray-800 border border-amber-500/30 rounded p-1 text-sm text-gray-200" > - + {/* */} - + {/* */}
From f9e5202c03605938e59c45691f4c7103029b2de1 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:38:24 +0700 Subject: [PATCH 087/107] Add SVG icons for Bitcoin, Tether, and XRP --- public/icons/btc.svg | 2 ++ public/icons/usdt.svg | 8 ++++++++ public/icons/xrp.svg | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 public/icons/btc.svg create mode 100644 public/icons/usdt.svg create mode 100644 public/icons/xrp.svg diff --git a/public/icons/btc.svg b/public/icons/btc.svg new file mode 100644 index 0000000..90ff5c5 --- /dev/null +++ b/public/icons/btc.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/public/icons/usdt.svg b/public/icons/usdt.svg new file mode 100644 index 0000000..fac5172 --- /dev/null +++ b/public/icons/usdt.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/icons/xrp.svg b/public/icons/xrp.svg new file mode 100644 index 0000000..47c57d4 --- /dev/null +++ b/public/icons/xrp.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From 606b600e9c904f2be41b0c406a2ef92906c6072f Mon Sep 17 00:00:00 2001 From: HungPhan-0612 <163500971+HungPhan-0612@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:57:15 +0700 Subject: [PATCH 088/107] Add border styling to navigation links in Header component --- components/Header.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/Header.tsx b/components/Header.tsx index 6e65ae1..ba342b6 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -263,49 +263,49 @@ const Header = () => {
) : ( - + Login )} From 98472a7abbc30d57a813ba7fa09a1d852959493b Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:18:08 +0700 Subject: [PATCH 089/107] chotdi --- components/Header.tsx | 2 +- package-lock.json | 361 +++++++++++++++++++++++++++++++----------- package.json | 1 + 3 files changed, 268 insertions(+), 96 deletions(-) diff --git a/components/Header.tsx b/components/Header.tsx index ba342b6..6914b0b 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -273,7 +273,7 @@ const Header = () => { className="text-sm uppercase hover:text-[#F5B056] transition border-b-[1px] border-gray-500" onClick={() => setIsOpen(false)} > - Pricetable + TopTokens =10" @@ -318,6 +318,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -332,6 +346,70 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", @@ -360,6 +438,43 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", @@ -511,6 +626,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helpers": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", @@ -2667,7 +2796,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3087,7 +3215,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3101,7 +3228,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3111,7 +3237,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3135,7 +3260,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -6265,7 +6389,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -6275,7 +6399,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -8553,7 +8677,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8581,7 +8704,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { @@ -8632,7 +8754,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -9085,7 +9206,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9221,7 +9341,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9363,6 +9482,39 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs58": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", @@ -9512,7 +9664,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9580,7 +9731,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -9605,7 +9755,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9841,7 +9990,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9859,6 +10007,13 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "license": "ISC" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT", + "peer": true + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -9954,7 +10109,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -10029,7 +10183,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -10572,7 +10725,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/diffie-hellman": { @@ -10602,7 +10754,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -10690,7 +10841,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/eccrypto": { @@ -10773,6 +10923,13 @@ "node": ">=4.0.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.119", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", + "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", + "license": "ISC", + "peer": true + }, "node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -10826,7 +10983,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/encode-utf8": { @@ -11131,6 +11287,16 @@ "@esbuild/win32-x64": "0.19.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12851,11 +13017,17 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -12884,7 +13056,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -13019,7 +13190,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -13113,7 +13283,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -13232,6 +13401,16 @@ "node": ">=8" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -13322,7 +13501,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -13343,7 +13521,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -13356,7 +13533,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -13366,7 +13542,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -13965,7 +14140,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -14017,7 +14191,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -14068,7 +14241,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14127,7 +14299,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -14179,7 +14350,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -14368,7 +14538,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -14417,7 +14586,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -14510,7 +14678,6 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -14773,7 +14940,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -14786,7 +14952,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lit": { @@ -15030,7 +15195,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -15040,7 +15204,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -15144,7 +15307,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -15247,7 +15409,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -15451,6 +15612,13 @@ "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT", + "peer": true + }, "node_modules/nodemailer": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", @@ -15530,7 +15698,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15869,7 +16036,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -15956,7 +16122,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15966,14 +16131,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -16024,7 +16187,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16123,7 +16285,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -16151,7 +16312,6 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -16180,7 +16340,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -16198,7 +16357,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -16218,7 +16376,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -16254,7 +16411,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -16280,7 +16436,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -16294,7 +16449,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/preact": { @@ -16480,7 +16634,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -16629,6 +16782,21 @@ "react": "*" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -16804,7 +16972,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -16828,7 +16995,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -16953,7 +17119,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -16994,7 +17159,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -17221,7 +17385,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -17519,7 +17682,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -17532,7 +17694,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17618,7 +17779,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -17820,7 +17980,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -17839,7 +17998,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17854,7 +18012,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17864,14 +18021,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17997,7 +18152,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -18014,7 +18168,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18027,7 +18180,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18104,7 +18256,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -18149,7 +18300,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -18205,7 +18355,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -18252,7 +18401,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -18269,7 +18417,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -18351,7 +18498,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -18361,7 +18507,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -18462,7 +18607,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -18494,7 +18638,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/ts-mixer": { @@ -18644,7 +18787,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18855,6 +18997,37 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -20308,7 +20481,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -20486,7 +20658,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -20505,7 +20676,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -20523,7 +20693,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -20533,14 +20702,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -20555,7 +20722,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -20568,7 +20734,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -20696,11 +20861,17 @@ "node": ">=0.10.32" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "peer": true + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 5c57f39..1c4fb19 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "react-hook-form": "^7.54.2", "react-hot-toast": "^2.5.2", "react-icons": "^5.4.0", + "react-intersection-observer": "^9.16.0", "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.2.0", "recharts": "^2.15.1", From 1ac34bd9f0e58fbd2fa47622aeb7b8e9f8209802 Mon Sep 17 00:00:00 2001 From: Woft257 <144596159+Woft257@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:11:02 +0700 Subject: [PATCH 090/107] Update NFT Address --- app/NFT/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/NFT/page.tsx b/app/NFT/page.tsx index 4221c81..06d014b 100644 --- a/app/NFT/page.tsx +++ b/app/NFT/page.tsx @@ -13,7 +13,7 @@ import PriceChart from '@/components/NFT/PriceChart'; import { toast } from '@/hooks/use-toast'; // Contract addresses -const NFT_CONTRACT_ADDRESS = "0xdf5d4038723f6605A3eCd7776FFe25f3b1Be39a0"; +const NFT_CONTRACT_ADDRESS = "0x2fF12fE4B3C4DEa244c4BdF682d572A90Df3B551"; const PATH_TOKEN_ADDRESS = "0xc3e9Cf26237c9002c0C04305D637AEa3d9A4A1DE"; const ITEMS_PER_PAGE = 8; @@ -652,4 +652,4 @@ export default function NFTMarketplace() { )}
); -} \ No newline at end of file +} From f17998e4941e647b8c37e9b3f94da594246f2946 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:20:18 +0700 Subject: [PATCH 091/107] Add email format validation and improve particles background performance --- app/api/subscribe/route.ts | 15 +- components/Footer.tsx | 435 ++++++++++++++++++---- components/ParticlesBackground.tsx | 45 ++- components/search/TransactionTable.tsx | 480 +++++++++++++++++-------- 4 files changed, 743 insertions(+), 232 deletions(-) diff --git a/app/api/subscribe/route.ts b/app/api/subscribe/route.ts index f827914..b836ad3 100644 --- a/app/api/subscribe/route.ts +++ b/app/api/subscribe/route.ts @@ -13,6 +13,16 @@ export async function POST(request: Request) { ); } + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + const errorMessage = language === 'en' ? 'Invalid email format' : 'Định dạng email không hợp lệ'; + return NextResponse.json( + { success: false, message: errorMessage }, + { status: 400 } + ); + } + // Nodemailer configuration const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'smtp.example.com', @@ -106,10 +116,13 @@ export async function POST(request: Request) { await transporter.sendMail(mailOptions); + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 500)); + // Success response based on language const successMessage = language === 'en' ? 'Subscription successful!' : 'Đăng ký thành công!'; return NextResponse.json( - { success: true, message: successMessage }, + { success: true, message: successMessage, data: { email, timestamp: new Date().toISOString() } }, { status: 200 } ); } catch (error) { diff --git a/components/Footer.tsx b/components/Footer.tsx index 1b53c47..8a0831a 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -1,103 +1,392 @@ -"use client"; // Ensures this runs on the client side +"use client"; + +import { useState, useEffect } from "react"; import Link from "next/link"; +import Image from "next/image"; +import { motion } from "framer-motion"; +import { ArrowUp, ChevronRight, ExternalLink } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; const Footer = () => { + const [showBackToTop, setShowBackToTop] = useState(false); + + // Email subscription states (matching page.tsx approach) + const [email, setEmail] = useState(''); + const [emailError, setEmailError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + // Toggle back-to-top button visibility based on scroll position + useEffect(() => { + const handleScroll = () => { + setShowBackToTop(window.scrollY > 500); + }; - return( -