diff --git a/app/NFT/collection/[collectionId]/page.tsx b/app/NFT/collection/[collectionId]/page.tsx
index 059d7d0..54b22af 100644
--- a/app/NFT/collection/[collectionId]/page.tsx
+++ b/app/NFT/collection/[collectionId]/page.tsx
@@ -225,7 +225,6 @@ export default function CollectionDetailsPage() {
// Add network as a filter attribute
attributeMap['Network'] = [
networkId === '0x1' ? 'Ethereum' :
- networkId === '0xaa36a7' ? 'Sepolia' :
networkId === '0x38' ? 'BNB Chain' :
'BNB Testnet'
];
@@ -339,7 +338,6 @@ export default function CollectionDetailsPage() {
const getExplorerLink = (address: string) => {
const chainConfig = {
'0x1': 'https://etherscan.io',
- '0xaa36a7': 'https://sepolia.etherscan.io',
'0x38': 'https://bscscan.com',
'0x61': 'https://testnet.bscscan.com',
};
@@ -352,7 +350,6 @@ export default function CollectionDetailsPage() {
const getNetworkName = () => {
const networks = {
'0x1': 'Ethereum',
- '0xaa36a7': 'Sepolia',
'0x38': 'BNB Chain',
'0x61': 'BNB Testnet',
};
diff --git a/app/NFT/collection/page.tsx b/app/NFT/collection/page.tsx
index 45e1427..11b9bae 100644
--- a/app/NFT/collection/page.tsx
+++ b/app/NFT/collection/page.tsx
@@ -202,17 +202,24 @@ export default function NFTCollectionPage() {
method: 'eth_chainId',
});
setChainId(chainId);
+ // Load collections immediately after setting chainId
+ loadCollections(chainId);
+ // Initial trending data - load for default period
+ loadTrendingCollections('24h');
} catch (error) {
console.error('Error checking network:', error);
+ // If there's an error, still load with default chainId
+ loadCollections(chainId);
+ loadTrendingCollections('24h');
}
+ } else {
+ // If no window.ethereum, load with default chainId
+ loadCollections(chainId);
+ loadTrendingCollections('24h');
}
};
checkNetwork();
- loadCollections(chainId);
-
- // Initial trending data
- loadTrendingCollections('24h');
}, []);
// Load user NFTs when account or chain changes
@@ -304,6 +311,29 @@ export default function NFTCollectionPage() {
// Load trending collections
const loadTrendingCollections = useCallback(async (period: string) => {
try {
+ // Create placeholder data with empty array if collections not loaded yet
+ if (collections.length === 0) {
+ // Set mock empty data with period to display the section even when data is loading
+ setTrendingData({
+ period,
+ data: [
+ {
+ id: 'loading-1',
+ name: 'Loading...',
+ imageUrl: '/images/placeholder-nft.png',
+ chain: chainId,
+ floorPrice: '0.00',
+ priceChange: 0,
+ volume: '0.00',
+ volumeChange: 0,
+ },
+ // Add more placeholder items as needed
+ ]
+ });
+ // Return early - real data will be loaded once collections are available
+ return;
+ }
+
// In real app this would fetch real data
const mockTrendingData = {
period,
@@ -323,7 +353,14 @@ export default function NFTCollectionPage() {
} catch (error) {
console.error('Error loading trending data:', error);
}
- }, [collections]);
+ }, [collections, chainId]);
+
+ useEffect(() => {
+ // When collections are loaded, update the trending data
+ if (collections.length > 0) {
+ loadTrendingCollections(trendingPeriod);
+ }
+ }, [collections, loadTrendingCollections, trendingPeriod]);
const handleNetworkChange = (networkId: string) => {
setChainId(networkId);
@@ -516,52 +553,63 @@ export default function NFTCollectionPage() {
- {trendingData.data.map((item, i) => (
- handleCardClick(item.id)}
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.3 + i * 0.05 }}
- >
- {i + 1} |
-
-
-
-
-
-
-
- {item.name}
- {(i === 0 || i === 2) && }
+ {trendingData.data.length > 0 ? (
+ trendingData.data.map((item, i) => (
+ handleCardClick(item.id)}
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.3 + i * 0.05 }}
+ >
+ {i + 1} |
+
+
+
+
-
- {getNetworkName(item.chain)}
+
+
+ {item.name}
+ {(i === 0 || i === 2) && }
+
+
+ {getNetworkName(item.chain)}
+
+ |
+
+ {item.floorPrice} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'}
+ |
+ = 0 ? 'text-green-500' : 'text-red-500'}`}>
+ {item.priceChange >= 0 ? '+' : ''}{item.priceChange.toFixed(2)}%
+ |
+
+ {item.volume} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'}
+ |
+ = 0 ? 'text-green-500' : 'text-red-500'}`}>
+ {item.volumeChange >= 0 ? '+' : ''}{item.volumeChange.toFixed(2)}%
+ |
+
+ ))
+ ) : (
+
+
+
+
+ Loading trending collections...
|
-
- {item.floorPrice} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'}
- |
- = 0 ? 'text-green-500' : 'text-red-500'}`}>
- {item.priceChange >= 0 ? '+' : ''}{item.priceChange.toFixed(2)}%
- |
-
- {item.volume} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'}
- |
- = 0 ? 'text-green-500' : 'text-red-500'}`}>
- {item.volumeChange >= 0 ? '+' : ''}{item.volumeChange.toFixed(2)}%
- |
-
- ))}
+
+ )}
|
diff --git a/app/api/alchemy-block-txns/route.ts b/app/api/alchemy-block-txns/route.ts
new file mode 100644
index 0000000..6d64c40
--- /dev/null
+++ b/app/api/alchemy-block-txns/route.ts
@@ -0,0 +1,114 @@
+import { NextResponse } from "next/server";
+
+const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
+const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
+
+interface Transaction {
+ blockHash: string;
+ blockNumber: string;
+ from: string;
+ gas: string;
+ gasPrice: string;
+ hash: string;
+ input: string;
+ nonce: string;
+ to: string;
+ transactionIndex: string;
+ value: string;
+ type: string;
+ timestamp: number;
+}
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const blockNumber = searchParams.get("blockNumber");
+
+ if (!blockNumber) {
+ return NextResponse.json(
+ { error: "Block number is required" },
+ { status: 400 }
+ );
+ }
+
+ try {
+ // First, get block data to get timestamp
+ const blockResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'eth_getBlockByNumber',
+ params: [
+ `0x${Number(blockNumber).toString(16)}`,
+ false
+ ]
+ })
+ });
+
+ const blockData = await blockResponse.json();
+
+ if (!blockData.result) {
+ return NextResponse.json(
+ { error: "Block not found" },
+ { status: 404 }
+ );
+ }
+
+ const timestamp = parseInt(blockData.result.timestamp, 16);
+
+ // Get all transactions from the block
+ const txnsResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'eth_getBlockByNumber',
+ params: [
+ `0x${Number(blockNumber).toString(16)}`,
+ true
+ ]
+ })
+ });
+
+ const txnsData = await txnsResponse.json();
+
+ if (!txnsData.result || !txnsData.result.transactions) {
+ return NextResponse.json(
+ { error: "Failed to fetch transactions" },
+ { status: 500 }
+ );
+ }
+
+ const transactions: Transaction[] = txnsData.result.transactions.map((tx: any) => ({
+ blockHash: tx.blockHash,
+ blockNumber: parseInt(tx.blockNumber, 16).toString(),
+ from: tx.from,
+ gas: parseInt(tx.gas, 16).toString(),
+ gasPrice: parseInt(tx.gasPrice, 16).toString(),
+ hash: tx.hash,
+ input: tx.input,
+ nonce: parseInt(tx.nonce, 16).toString(),
+ to: tx.to || '0x0', // Contract creation if no 'to' address
+ transactionIndex: parseInt(tx.transactionIndex, 16).toString(),
+ value: (parseInt(tx.value, 16) / 1e18).toString(), // Convert from Wei to ETH
+ type: parseInt(tx.type, 16).toString(),
+ timestamp: timestamp
+ }));
+
+ return NextResponse.json({
+ blockNumber,
+ timestamp,
+ transactions,
+ total: transactions.length
+ });
+
+ } catch (error) {
+ console.error("Error fetching block transactions:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch block transactions" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/alchemy-block/route.ts b/app/api/alchemy-block/route.ts
new file mode 100644
index 0000000..58e89c1
--- /dev/null
+++ b/app/api/alchemy-block/route.ts
@@ -0,0 +1,84 @@
+import { NextResponse } from "next/server";
+
+const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
+const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
+
+interface BlockDetails {
+ number: string;
+ hash: string;
+ parentHash: string;
+ timestamp: string;
+ nonce: string;
+ difficulty: string;
+ gasLimit: string;
+ gasUsed: string;
+ miner: string;
+ baseFeePerGas: string;
+ extraData: string;
+ transactions: string[];
+ size: string;
+}
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const blockNumber = searchParams.get("blockNumber");
+ const blockHash = searchParams.get("blockHash");
+
+ if (!blockNumber && !blockHash) {
+ return NextResponse.json(
+ { error: "Block number or hash is required" },
+ { status: 400 }
+ );
+ }
+
+ try {
+ const blockResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'eth_getBlockByNumber',
+ params: [
+ blockNumber ? `0x${Number(blockNumber).toString(16)}` : blockHash,
+ true
+ ]
+ })
+ });
+
+ const blockData = await blockResponse.json();
+
+ if (!blockData.result) {
+ return NextResponse.json(
+ { error: "Block not found" },
+ { status: 404 }
+ );
+ }
+
+ const block: BlockDetails = {
+ number: parseInt(blockData.result.number, 16).toString(),
+ hash: blockData.result.hash,
+ parentHash: blockData.result.parentHash,
+ timestamp: new Date(parseInt(blockData.result.timestamp, 16) * 1000).toISOString(),
+ nonce: blockData.result.nonce,
+ difficulty: parseInt(blockData.result.difficulty, 16).toString(),
+ gasLimit: parseInt(blockData.result.gasLimit, 16).toString(),
+ gasUsed: parseInt(blockData.result.gasUsed, 16).toString(),
+ miner: blockData.result.miner,
+ baseFeePerGas: blockData.result.baseFeePerGas
+ ? parseInt(blockData.result.baseFeePerGas, 16).toString()
+ : "0",
+ extraData: blockData.result.extraData,
+ transactions: blockData.result.transactions.map((tx: any) => tx.hash),
+ size: parseInt(blockData.result.size, 16).toString()
+ };
+
+ return NextResponse.json(block);
+ } catch (error) {
+ console.error("Error fetching block details:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch block details" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/alchemy-token/route.ts b/app/api/alchemy-token/route.ts
new file mode 100644
index 0000000..042dd8e
--- /dev/null
+++ b/app/api/alchemy-token/route.ts
@@ -0,0 +1,170 @@
+import { NextResponse } from "next/server";
+
+const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
+const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
+
+interface TokenMetadata {
+ name: string;
+ symbol: string;
+ decimals: number;
+ logo: string | null;
+ totalSupply?: string;
+}
+
+interface TokenTransfer {
+ hash: string;
+ from: string;
+ to: string;
+ value: string;
+ blockNumber: string;
+ timestamp: string;
+}
+
+interface TokenDetails {
+ name: string;
+ symbol: string;
+ decimals: number;
+ address: string;
+ totalSupply: string;
+ logo: string;
+ holders: number;
+ transfers: number;
+ lastUpdated: string;
+ contractDeployed: string;
+ implementation?: string;
+ isProxy: boolean;
+ recentTransfers: TokenTransfer[];
+ priceUSD?: string;
+ volume24h?: string;
+ marketCap?: string;
+}
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const address = searchParams.get("address");
+
+ if (!address) {
+ return NextResponse.json({ error: "Token address is required" }, { status: 400 });
+ }
+
+ try {
+ // Get token metadata
+ const metadataResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'alchemy_getTokenMetadata',
+ params: [address]
+ })
+ });
+ const metadataData = await metadataResponse.json();
+
+ // Get token total supply
+ const supplyResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 2,
+ method: 'eth_call',
+ params: [{
+ to: address,
+ data: '0x18160ddd' // totalSupply()
+ }, 'latest']
+ })
+ });
+ const supplyData = await supplyResponse.json();
+
+ // Get recent token transfers
+ const transfersResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 3,
+ method: 'alchemy_getAssetTransfers',
+ params: [{
+ fromBlock: "0x0",
+ toBlock: "latest",
+ contractAddresses: [address],
+ category: ["erc20"],
+ withMetadata: true,
+ maxCount: "0x14", // Fetch last 20 transfers
+ order: "desc"
+ }]
+ })
+ });
+ const transfersData = await transfersResponse.json();
+
+ // Format recent transfers
+ const recentTransfers = transfersData.result?.transfers?.map((transfer: any) => ({
+ hash: transfer.hash,
+ from: transfer.from,
+ to: transfer.to,
+ value: transfer.value,
+ blockNumber: transfer.blockNum,
+ timestamp: transfer.metadata.blockTimestamp
+ })) || [];
+
+ // Get price data from CoinGecko
+ let priceData = null;
+ try {
+ const priceResponse = await fetch(
+ `https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=${address}&vs_currencies=usd&include_24hr_vol=true&include_market_cap=true`
+ );
+ priceData = await priceResponse.json();
+ } catch (e) {
+ console.error("Error fetching price data:", e);
+ }
+
+ // Get contract creation info
+ const deploymentResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 4,
+ method: 'eth_getCode',
+ params: [address, 'latest']
+ })
+ });
+ const deploymentData = await deploymentResponse.json();
+ const isProxy = deploymentData.result?.length > 2; // Simple check for proxy contract
+
+ if (!metadataData.result || !supplyData.result) {
+ return NextResponse.json({ error: "Token not found" }, { status: 404 });
+ }
+
+ const metadata: TokenMetadata = metadataData.result;
+ const totalSupply = parseInt(supplyData.result, 16).toString();
+ const transfers = recentTransfers.length;
+
+ const tokenDetails: TokenDetails = {
+ name: metadata.name || 'Unknown Token',
+ symbol: metadata.symbol || 'UNKNOWN',
+ decimals: metadata.decimals || 18,
+ address: address,
+ totalSupply,
+ logo: metadata.logo || '/placeholder-token.png',
+ holders: 0, // Would need separate API call
+ transfers,
+ lastUpdated: new Date().toISOString(),
+ contractDeployed: new Date().toISOString(),
+ isProxy,
+ recentTransfers,
+ priceUSD: priceData?.[address.toLowerCase()]?.usd?.toString(),
+ volume24h: priceData?.[address.toLowerCase()]?.usd_24h_vol?.toString(),
+ marketCap: priceData?.[address.toLowerCase()]?.usd_market_cap?.toString()
+ };
+
+ return NextResponse.json(tokenDetails);
+ } catch (error) {
+ console.error("Error fetching token details:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch token details" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/alchemy-txnhash/route.ts b/app/api/alchemy-txnhash/route.ts
new file mode 100644
index 0000000..9b93fc6
--- /dev/null
+++ b/app/api/alchemy-txnhash/route.ts
@@ -0,0 +1,95 @@
+import { NextResponse } from "next/server";
+
+const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
+const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`;
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const hash = searchParams.get("hash");
+
+ if (!hash) {
+ return NextResponse.json({ error: "Transaction hash is required" }, { status: 400 });
+ }
+
+ try {
+ // Get transaction details
+ const txResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'eth_getTransactionByHash',
+ params: [hash]
+ })
+ });
+ const txData = await txResponse.json();
+
+ // Get transaction receipt
+ const receiptResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 2,
+ method: 'eth_getTransactionReceipt',
+ params: [hash]
+ })
+ });
+ const receiptData = await receiptResponse.json();
+
+ if (!txData.result || !receiptData.result) {
+ return NextResponse.json({ error: "Transaction not found" }, { status: 404 });
+ }
+
+ // Get block information
+ const blockResponse = await fetch(ALCHEMY_API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 3,
+ method: 'eth_getBlockByHash',
+ params: [txData.result.blockHash, false]
+ })
+ });
+ const blockData = await blockResponse.json();
+
+ // Convert hex values to decimal
+ const value = parseInt(txData.result.value, 16);
+ const gasPrice = parseInt(txData.result.gasPrice, 16);
+ const gasUsed = parseInt(receiptData.result.gasUsed, 16);
+ const gasLimit = parseInt(txData.result.gas, 16);
+ const timestamp = parseInt(blockData.result.timestamp, 16);
+
+ // Format the transaction data
+ const transaction = {
+ hash: txData.result.hash,
+ from: txData.result.from,
+ to: txData.result.to,
+ value: value.toString(),
+ valueInEth: (value / 1e18).toFixed(6),
+ gasPrice: gasPrice.toString(),
+ gasLimit: gasLimit.toString(),
+ gasUsed: gasUsed.toString(),
+ nonce: parseInt(txData.result.nonce, 16),
+ status: receiptData.result.status === '0x1' ? "Success" : "Failed",
+ timestamp: timestamp,
+ blockNumber: parseInt(txData.result.blockNumber, 16),
+ blockHash: txData.result.blockHash,
+ confirmations: parseInt(receiptData.result.confirmations || '0', 16),
+ effectiveGasPrice: parseInt(receiptData.result.effectiveGasPrice, 16).toString(),
+ type: parseInt(txData.result.type, 16),
+ data: txData.result.input,
+ txFee: ((gasUsed * parseInt(receiptData.result.effectiveGasPrice, 16)) / 1e18).toFixed(6),
+ };
+
+ return NextResponse.json(transaction);
+ } catch (error) {
+ console.error("Error fetching transaction:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch transaction details" },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/block-txns/page.tsx b/app/block-txns/page.tsx
new file mode 100644
index 0000000..fc73da0
--- /dev/null
+++ b/app/block-txns/page.tsx
@@ -0,0 +1,271 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
+import { motion } from "framer-motion";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Loader2, ArrowLeft,
+ ChevronLeft, ChevronRight
+} from "lucide-react";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import ParticlesBackground from "@/components/ParticlesBackground";
+
+interface Transaction {
+ blockHash: string;
+ blockNumber: string;
+ from: string;
+ gas: string;
+ gasPrice: string;
+ hash: string;
+ input: string;
+ nonce: string;
+ to: string;
+ transactionIndex: string;
+ value: string;
+ type: string;
+ timestamp: number;
+}
+
+interface BlockData {
+ blockNumber: string;
+ timestamp: number;
+ transactions: Transaction[];
+ total: number;
+}
+
+const ITEMS_PER_PAGE = 20;
+
+export default function BlockTransactions() {
+ const searchParams = useSearchParams();
+ const blockNumber = searchParams.get("number");
+ const router = useRouter();
+
+ const [blockData, setBlockData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [page, setPage] = useState(1);
+
+ useEffect(() => {
+ const fetchBlockData = async () => {
+ if (!blockNumber) {
+ setError("Block number is required");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/alchemy-block-txns?blockNumber=${blockNumber}`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || "Failed to fetch block data");
+ }
+
+ setBlockData(data);
+ setError(null);
+ } catch (err) {
+ console.error("Error fetching block data:", err);
+ setError(err instanceof Error ? err.message : "Failed to fetch block data");
+ setBlockData(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ setLoading(true);
+ fetchBlockData();
+ }, [blockNumber]);
+
+ if (loading) {
+ return (
+ <>
+
+
+
+
+
+ Loading Block Transactions...
+
+
+
+ >
+ );
+ }
+
+ if (error || !blockData) {
+ return (
+ <>
+
+
+
+
+
+
{error || "Block data not found"}
+
+
+
+
+
+ >
+ );
+ }
+
+ const totalTransactions = blockData.total;
+ const totalPages = Math.ceil(totalTransactions / ITEMS_PER_PAGE);
+ const currentPage = Math.min(Math.max(1, page), totalPages);
+ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
+ const endIndex = Math.min(startIndex + ITEMS_PER_PAGE, totalTransactions);
+ const paginatedTransactions = blockData.transactions.slice(startIndex, endIndex);
+
+ return (
+ <>
+
+
+
+
+
+
+ Block #{blockData.blockNumber}
+
+ {totalTransactions} Transactions
+
+
+
+ {new Date(blockData.timestamp * 1000).toLocaleString()}
+
+
+
+
+
+
+ {totalTransactions > 0 ? (
+ <>
+
+
+
+
+ Tx Hash
+ From
+ To
+ Value (ETH)
+ Gas Price (Gwei)
+ Gas Limit
+
+
+
+ {paginatedTransactions.map((tx) => (
+
+
+
+
+
+
+
+
+
+
+
+ {Number(tx.value).toFixed(12)}
+
+
+ {(Number(tx.gasPrice) / 1e9).toFixed(2)}
+
+
+ {Number(tx.gas).toLocaleString()}
+
+
+ ))}
+
+
+
+
+ {totalPages > 1 && (
+
+
+ Showing {startIndex + 1}-{endIndex} of {totalTransactions}
+
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+
+ )}
+ >
+ ) : (
+
+ No transactions found in this block
+
+ )}
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/block/page.tsx b/app/block/page.tsx
new file mode 100644
index 0000000..5a00745
--- /dev/null
+++ b/app/block/page.tsx
@@ -0,0 +1,253 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
+import { motion } from "framer-motion";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Loader2, Copy, ExternalLink,
+ Clock, Hash, ChevronUp,
+ Cpu, Fuel, Pickaxe,
+ Database, Layers
+} from "lucide-react";
+import { toast } from "sonner";
+import { format } from "date-fns";
+import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
+import "react-circular-progressbar/dist/styles.css";
+import ParticlesBackground from "@/components/ParticlesBackground";
+
+interface BlockDetails {
+ number: string;
+ hash: string;
+ parentHash: string;
+ timestamp: string;
+ nonce: string;
+ difficulty: string;
+ gasLimit: string;
+ gasUsed: string;
+ miner: string;
+ baseFeePerGas: string;
+ extraData: string;
+ transactions: string[];
+ size: string;
+}
+
+const InfoCard = ({ title, icon: Icon, children }: any) => (
+
+
+
+
{title}
+
+ {children}
+
+);
+
+export default function BlockDetails() {
+ const searchParams = useSearchParams();
+ const blockNumber = searchParams.get("number");
+ const router = useRouter();
+ const [block, setBlock] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchBlock = async () => {
+ if (!blockNumber) {
+ setError("Block number is required");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/alchemy-block?blockNumber=${blockNumber}`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || "Failed to fetch block details");
+ }
+
+ setBlock(data);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch block details");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchBlock();
+ }, [blockNumber]);
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success("Copied to clipboard!");
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Loading Block Details...
+
+
+
+ );
+ }
+
+ if (error || !block) {
+ return (
+
+
+
+
+
{error || "Block not found"}
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ #{block.number}
+
+
+
+
+
+ {format(new Date(block.timestamp), "PPpp")}
+
+
+
+
+
+
+ {block.hash}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Circular Progress Bar */}
+
+
+
+
+ {/* Gas Usage Details */}
+
+
+ {Number(block.gasUsed).toLocaleString()} / {Number(block.gasLimit).toLocaleString()}
+
+ ({((Number(block.gasUsed) / Number(block.gasLimit)) * 100).toFixed(2)}%)
+
+
+
+{((Number(block.gasUsed) / Number(block.gasLimit)) * 100 - 100).toFixed(2)}% Gas Target
+
+
+
+
+
+
+
+
+
+
+ {(Number(block.baseFeePerGas) / 1e9).toFixed(2)} Gwei
+
+
+
+
+
+ {Number(block.size).toLocaleString()} bytes
+
+
+
+
+
+
+ Transactions
+
+ {block.transactions.length}
+
+
+
+
+ {block.transactions.slice(0, 9).map((hash) => (
+
+ ))}
+
+ {block.transactions.length > 9 && (
+
+
+
+ )}
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/token/page.tsx b/app/token/page.tsx
new file mode 100644
index 0000000..fda2f57
--- /dev/null
+++ b/app/token/page.tsx
@@ -0,0 +1,388 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
+import { motion } from "framer-motion";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import ParticlesBackground from "@/components/ParticlesBackground";
+import {
+ Loader2, Copy, ExternalLink,
+ Coins, Users, ArrowLeftRight,
+ Calendar, Shield, Clock,
+ TrendingUp, DollarSign, BarChart3,
+ ArrowUpRight, ArrowDownRight
+} from "lucide-react";
+import { toast } from "sonner";
+import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table";
+
+// Types
+interface TokenTransfer {
+ hash: string;
+ from: string;
+ to: string;
+ value: string;
+ blockNumber: string;
+ timestamp: string;
+}
+
+interface TokenDetails {
+ name: string;
+ symbol: string;
+ decimals: number;
+ address: string;
+ totalSupply: string;
+ logo: string;
+ holders: number;
+ transfers: number;
+ lastUpdated: string;
+ contractDeployed: string;
+ implementation?: string;
+ isProxy: boolean;
+ recentTransfers: TokenTransfer[];
+ priceUSD?: string;
+ volume24h?: string;
+ marketCap?: string;
+}
+
+// Helper Functions
+const formatNumber = (num: number | string, useCommas = true) => {
+ if (!useCommas) return Number(num).toString();
+ return new Intl.NumberFormat().format(Number(num));
+ };
+
+const formatUSD = (value: string) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ }).format(Number(value));
+};
+
+// Components
+const TokenStatCard = ({
+ icon: Icon,
+ title,
+ value,
+ delay
+}: {
+ icon: any;
+ title: string;
+ value: React.ReactNode;
+ delay: number;
+}) => (
+
+
+
+
{title}
+
+ {value}
+
+);
+
+// Main Component
+export default function TokenPage() {
+ const searchParams = useSearchParams();
+ const address = searchParams.get("address");
+ const [token, setToken] = useState(null);
+ const router = useRouter();
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchToken = async () => {
+ if (!address) {
+ setError("Token address is required");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/alchemy-token?address=${address}`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || "Failed to fetch token details");
+ }
+
+ setToken(data);
+ } catch (err) {
+ console.error("Error fetching token:", err);
+ setError(err instanceof Error ? err.message : "Failed to fetch token details");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchToken();
+ }, [address]);
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success("Copied to clipboard!");
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Loading Token Details...
+
+
+
+ );
+ }
+
+ if (error || !token) {
+ return (
+
+
+
+
+
{error || "Token not found"}
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+

{
+ e.currentTarget.src = '/placeholder-token.png';
+ }}
+ />
+ {token.priceUSD && (
+
+ ${Number(token.priceUSD).toFixed(2)}
+
+ )}
+
+
+
+ {token.name}
+
+ {token.symbol}
+
+
+
+ {token.address}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatNumber(Number(token.totalSupply) / Math.pow(10, token.decimals))}
+ {token.symbol}
+ >
+ }
+ delay={0.2}
+ />
+
+
+
+ {token.marketCap && (
+
+ )}
+
+ {token.volume24h && (
+
+ )}
+
+
+
+
+ {token.recentTransfers && token.recentTransfers.length > 0 && (
+
+ Recent Transfers
+
+
+
+
+ Transaction
+ Block
+ Type
+ From
+ To
+ Amount
+ Time
+
+
+
+ {token.recentTransfers.map((transfer) => (
+
+
+
+
+
+
+
+
+ {transfer.from === token.address ? (
+
+
+ Out
+
+ ) : (
+
+
+ In
+
+ )}
+
+
+
+
+
+
+
+
+
+ {transfer.from === token.address ? "-" : "+"}
+
+ {/* {(Number(transfer.value) / Math.pow(10, token.decimals))} {token.symbol} */}
+ {transfer.value} {token.symbol}
+ {token.priceUSD && (
+
+ {formatUSD((Number(transfer.value) / Math.pow(10, token.decimals) * Number(token.priceUSD)).toString())}
+
+ )}
+
+
+ {new Date(transfer.timestamp).toLocaleString()}
+
+
+ ))}
+
+
+
+
+ )}
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/txn-hash/page.tsx b/app/txn-hash/page.tsx
new file mode 100644
index 0000000..ca5bea7
--- /dev/null
+++ b/app/txn-hash/page.tsx
@@ -0,0 +1,323 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useSearchParams,useRouter } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Loader2, ExternalLink, Copy, XCircle } from "lucide-react";
+import { toast } from "sonner";
+import { format } from "date-fns";
+import { motion } from "framer-motion";
+import { ArrowRight, ArrowDownRight } from "lucide-react";
+import ParticlesBackground from "@/components/ParticlesBackground";
+
+interface TransactionDetails {
+ hash: string;
+ from: string;
+ to: string;
+ value: string;
+ valueInEth: string;
+ gasPrice: string;
+ gasLimit: string;
+ gasUsed: string;
+ nonce: number;
+ status: string;
+ timestamp: number;
+ blockNumber: number;
+ blockHash: string;
+ confirmations: number;
+ effectiveGasPrice: string;
+ type: number;
+ data: string;
+ txFee: string;
+}
+
+export default function TransactionDetails() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const hash = searchParams.get("hash");
+ const [transaction, setTransaction] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const formatGwei = (wei: string) => {
+ return (Number(wei) / 1e9).toFixed(2) + " Gwei";
+ };
+
+ useEffect(() => {
+ const fetchTransaction = async () => {
+ if (!hash) {
+ setError("Transaction hash is required");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/alchemy-txnhash/?hash=${hash}`);
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || "Failed to fetch transaction details");
+ }
+
+ setTransaction(data);
+ } catch (err) {
+ console.error("Error fetching transaction:", err);
+ setError(err instanceof Error ? err.message : "Failed to fetch transaction details");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTransaction();
+ }, [hash]);
+
+ const getExplorerUrl = () => {
+ return `https://etherscan.io/tx/${hash}`;
+ };
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success("Copied to clipboard!");
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Loading Transaction Details...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
Transaction Error
+
{error}
+
+
+
+ );
+ }
+
+ if (!transaction) return null;
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Transaction Details
+
+
+ {transaction.status}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Transaction Basic Info */}
+
+
+
Transaction Hash
+
+
{transaction.hash}
+
+
+ {/* From/To Section */}
+
+
+
+
+
+
+
+
+
+
+ {/* Value Section */}
+
+
Value
+
+ {transaction.valueInEth} ETH
+
+ (${(Number(transaction.valueInEth) * 2000).toFixed(4)})
+
+
+
+
+
+
+ {/* Gas Information */}
+
+
Gas Information
+
+
+ Gas Price:
+ {formatGwei(transaction.gasPrice)}
+
+
+ Gas Limit:
+ {transaction.gasLimit}
+
+
+ Gas Used:
+
+ {transaction.gasUsed} ({(Number(transaction.gasUsed) / Number(transaction.gasLimit) * 100).toFixed(2)}%)
+
+
+
+ Effective Gas Price:
+ {formatGwei(transaction.effectiveGasPrice)}
+
+
+ Transaction Fee:
+ {transaction.txFee+"ETH"}
+
+
+
+
+ {/* Block Information */}
+
+
Block Information
+
+
+ Block Number:
+ #{transaction.blockNumber}
+
+
+ Block Hash:
+ {transaction.blockHash}
+
+
+ Confirmations:
+ {transaction.confirmations}
+
+
+ Timestamp:
+
+ {format(new Date(transaction.timestamp * 1000), "PPpp")}
+
+
+
+
+
+ {/* Transaction Details */}
+
+
Transaction Details
+
+
+ Nonce:
+ {transaction.nonce}
+
+
+ Type:
+ {transaction.type}
+
+
+
+
+
+
+ {/* Transaction Data Section */}
+
+ Input Data
+
+
+ {transaction.data}
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/components/NFT/AnimatedNFTCard.tsx b/components/NFT/AnimatedNFTCard.tsx
index a48e841..688d03f 100644
--- a/components/NFT/AnimatedNFTCard.tsx
+++ b/components/NFT/AnimatedNFTCard.tsx
@@ -4,6 +4,8 @@ import { ExternalLink } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { getChainColorTheme } from '@/lib/api/chainProviders';
import LazyImage from './LazyImage';
+import { ipfsUriToGatewayUrl } from '@/lib/utils/ipfsUtils';
+import Image from "next/legacy/image";
interface NFT {
id: string;
@@ -30,6 +32,9 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized
const [imageLoaded, setImageLoaded] = useState(false);
const cardRef = useRef(null);
+ // Process image URL for IPFS compatibility
+ const imageUrl = nft.imageUrl ? ipfsUriToGatewayUrl(nft.imageUrl) : '';
+
// Chain-specific styling
const chainTheme = getChainColorTheme(nft.chain);
@@ -216,13 +221,14 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized
{/* Network Badge - Positioned absolutely top-right */}
-
-
+
@@ -235,11 +241,11 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized
{/* NFT Image with progressive loading */}
setImageLoaded(true)}
onError={() => {/* Error handled inside LazyImage */}}
@@ -264,16 +270,36 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized
{/* Attributes */}
- {nft.attributes?.slice(0, 3).map((attr, i) => (
-
- {attr.trait_type === 'Network' ? null : `${attr.trait_type}: ${attr.value}`}
-
- ))}
+ {(() => {
+ const processAttributes = () => {
+ let attrs = nft.attributes;
+ if (attrs && !Array.isArray(attrs) && typeof attrs === 'object') {
+ attrs = Object.entries(attrs).map(([trait_type, value]) => ({
+ trait_type,
+ value: String(value)
+ }));
+ }
+ return (Array.isArray(attrs) ? attrs : [])
+ .filter(attr =>
+ attr &&
+ typeof attr === 'object' &&
+ 'trait_type' in attr &&
+ 'value' in attr
+ )
+ .slice(0, 3);
+ };
+
+ return processAttributes().map((attr, i) => (
+
+ {attr.trait_type === 'Network' ? null : `${attr.trait_type}: ${attr.value}`}
+
+ ));
+ })()}
diff --git a/components/NFT/CollectionCard3D.tsx b/components/NFT/CollectionCard3D.tsx
index b95d9f2..9cf1c51 100644
--- a/components/NFT/CollectionCard3D.tsx
+++ b/components/NFT/CollectionCard3D.tsx
@@ -1,5 +1,5 @@
import { useRef, useState } from 'react';
-import Image from 'next/image';
+import Image from 'next/legacy/image';
import Link from 'next/link';
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { Sparkles, Verified, Users, ExternalLink, ChevronRight } from 'lucide-react';
@@ -23,7 +23,7 @@ interface CollectionCardProps {
onClick?: () => void;
}
-export default function CollectionCard3D({ collection, index, onClick }: CollectionCardProps) {
+export default function CollectionCard3D({ collection, index = 0, onClick }: CollectionCardProps) {
const cardRef = useRef
(null);
const [isHovered, setIsHovered] = useState(false);
const [imgLoaded, setImgLoaded] = useState(false);
@@ -114,17 +114,49 @@ export default function CollectionCard3D({ collection, index, onClick }: Collect
},
};
- // Get network name
- const getNetworkName = () => {
- const networks: Record = {
- '0x1': 'Ethereum',
- '0xaa36a7': 'Sepolia',
- '0x38': 'BNB Chain',
- '0x61': 'BNB Testnet'
- };
- return networks[collection.chain] || 'Unknown Network';
+ // Get network badge details
+ const getNetworkBadge = () => {
+ switch (collection.chain) {
+ case '0x1':
+ return {
+ icon: '/icons/eth.svg',
+ name: 'ETH',
+ bgClass: 'bg-blue-500/20',
+ textColor: '#6b8df7'
+ };
+ case '0xaa36a7':
+ return {
+ icon: '/icons/eth.svg',
+ name: 'Sepolia',
+ bgClass: 'bg-blue-400/20',
+ textColor: '#8aa2f2'
+ };
+ case '0x38':
+ return {
+ icon: '/icons/bnb.svg',
+ name: 'BNB',
+ bgClass: 'bg-yellow-500/20',
+ textColor: '#F0B90B'
+ };
+ case '0x61':
+ return {
+ icon: '/icons/bnb.svg',
+ name: 'BNB Testnet',
+ bgClass: 'bg-yellow-400/20',
+ textColor: '#F5CA3B'
+ };
+ default:
+ return {
+ icon: '/icons/eth.svg',
+ name: 'ETH',
+ bgClass: 'bg-blue-500/20',
+ textColor: '#6b8df7'
+ };
+ }
};
+ const networkBadge = getNetworkBadge();
+
return (
-
-
+
+
+ {/* Fix: Update the icon container to ensure proper display */}
+
- {getNetworkName()}
-
+
+ {networkBadge.name}
+
+
@@ -221,7 +253,7 @@ export default function CollectionCard3D({ collection, index, onClick }: Collect
diff --git a/components/NFT/LazyImage.tsx b/components/NFT/LazyImage.tsx
index ee1259b..78ec9a3 100644
--- a/components/NFT/LazyImage.tsx
+++ b/components/NFT/LazyImage.tsx
@@ -1,160 +1,73 @@
-import { useState, useEffect, useRef } from 'react';
-import { motion } from 'framer-motion';
-import Image from 'next/image';
+import { useState, useEffect } from 'react';
+import Image, { ImageProps } from 'next/legacy/image';
import { Info } from 'lucide-react';
-interface LazyImageProps {
+interface LazyImageProps extends Omit {
src: string;
- alt: string;
- className?: string;
- width?: number;
- height?: number;
- priority?: boolean;
- fill?: boolean;
- objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
- placeholder?: 'blur' | 'empty';
- blurDataURL?: string;
- sizes?: string;
- quality?: number;
- onLoad?: () => void;
- onError?: () => void;
+ showLoadingIndicator?: boolean;
+ objectFit?: "fill" | "contain" | "cover" | "none" | "scale-down";
}
-export default function LazyImage({
- src,
- alt,
- className = '',
- width,
- height,
- priority = false,
- fill = false,
- objectFit = 'cover',
- placeholder = 'empty',
- blurDataURL,
- sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
- quality = 75,
- onLoad,
+export default function LazyImage({
+ src,
+ alt,
+ showLoadingIndicator = false,
onError,
+ onLoad,
+ objectFit = "cover",
+ ...props
}: LazyImageProps) {
- const [isLoaded, setIsLoaded] = useState(false);
- const [isInView, setIsInView] = useState(false);
- const [imgSrc, setImgSrc] = useState(null);
+ const [imgSrc, setImgSrc] = useState(src);
+ const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
- const imgRef = useRef(null);
- const placeholderColors = useRef([
- 'rgb(30, 30, 30)', 'rgb(40, 40, 40)', 'rgb(50, 50, 50)', 'rgb(35, 35, 35)'
- ]);
-
- // Function to transform IPFS URLs
- const transformUrl = (url: string): string => {
- if (!url) return '';
- if (url.startsWith('ipfs://')) {
- return `https://ipfs.io/ipfs/${url.slice(7)}`;
- }
- return url;
- };
-
- // Set up intersection observer to detect when image is in viewport
- useEffect(() => {
- if (!imgRef.current || priority) {
- setIsInView(true);
- return;
- }
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- setIsInView(entry.isIntersecting);
- },
- {
- root: null,
- rootMargin: '200px', // Load images 200px before they appear in viewport
- threshold: 0.01,
- }
- );
-
- observer.observe(imgRef.current);
-
- return () => {
- if (imgRef.current) {
- observer.unobserve(imgRef.current);
- }
- };
- }, [priority]);
-
- // Set image source when in view
+
useEffect(() => {
- if (isInView && src) {
- setImgSrc(transformUrl(src));
- }
- }, [isInView, src]);
-
- // Handle image load
- const handleImageLoad = () => {
- setIsLoaded(true);
- if (onLoad) onLoad();
- };
-
- // Handle image error
- const handleImageError = () => {
+ setImgSrc(src);
+ setLoading(true);
+ setError(false);
+ }, [src]);
+
+ const handleError = () => {
+ setImgSrc('/images/placeholder-nft.png'); // Fallback image
setError(true);
- setIsLoaded(true);
- if (onError) onError();
+ setLoading(false);
+ // Fix: Don't pass Error object directly to onError
+ if (onError) onError({} as React.SyntheticEvent);
+ };
+
+ const handleLoad = (event: any) => {
+ setLoading(false);
+ if (onLoad) onLoad(event);
};
-
- // Generate random placeholder background
- const placeholderBackground = `linear-gradient(45deg, ${placeholderColors.current[0]}, ${placeholderColors.current[1]}, ${placeholderColors.current[2]}, ${placeholderColors.current[3]})`;
+
+ // Handle IPFS and data URLs properly
+ // Fix: Changed from let to const
+ const finalSrc = imgSrc;
+
+ if (!imgSrc || error) {
+ return (
+
+
+
+ );
+ }
return (
-
- {/* Placeholder with shimmer effect */}
- {!isLoaded && (
-
- )}
-
- {/* Main image */}
- {imgSrc && isInView && (
-
- )}
-
- {/* Error fallback */}
- {error && (
+ <>
+ {loading && showLoadingIndicator && (
)}
-
+
+
+ >
);
}
diff --git a/components/NFT/NetworkSelector.tsx b/components/NFT/NetworkSelector.tsx
index 28be75c..7a2c390 100644
--- a/components/NFT/NetworkSelector.tsx
+++ b/components/NFT/NetworkSelector.tsx
@@ -33,14 +33,6 @@ const networks: Network[] = [
color: 'bg-blue-500/20 border-blue-500/50',
hexColor: '#6b8df7'
},
- {
- id: '0xaa36a7',
- name: 'Sepolia',
- icon: '/icons/eth.svg',
- color: 'bg-blue-400/20 border-blue-400/50',
- hexColor: '#8aa2f2',
- testnet: true
- },
{
id: '0x38',
name: 'BNB Chain',
diff --git a/components/search-offchain/SearchBarOffChain.tsx b/components/search-offchain/SearchBarOffChain.tsx
index 4f0315e..f0e4abd 100644
--- a/components/search-offchain/SearchBarOffChain.tsx
+++ b/components/search-offchain/SearchBarOffChain.tsx
@@ -4,7 +4,7 @@ import { useState } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
-import { Search, X, Globe, AlertTriangle } from "lucide-react"
+import { Search, X, Globe, AlertTriangle, Database, Hash, Coins, Layers } from "lucide-react"
import { LoadingScreen } from "@/components/loading-screen"
import Neo4jIcon from "@/components/icons/Neo4jIcon"
import {
@@ -20,7 +20,7 @@ import { toast } from "sonner"
export type NetworkType = "mainnet" | "optimism" | "arbitrum"
export type ProviderType = "etherscan" | "infura"
-
+type InputType = "ADDRESS" | "TRANSACTION-HASH" | "TOKEN" | "BLOCK" | "NEO4J" | "UNKNOWN";
// Ethereum address validation regex pattern
const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
@@ -30,10 +30,66 @@ export default function SearchBar() {
const [addressError, setAddressError] = useState(null)
const router = useRouter()
- const [searchType, setSearchType] = useState<"onchain" | "offchain">("offchain")
+ const [searchType, setSearchType] = useState<"onchain" | "offchain"| "Txn Hash" | "Token" | "Block" | "All">("offchain")
const [network, setNetwork] = useState("mainnet")
const [provider, setProvider] = useState("etherscan")
+ const detectInputType = (input: string): InputType => {
+ // Clean the input
+ const cleanInput = input.trim().toLowerCase();
+
+ // Check for empty input
+ if (!cleanInput) return "UNKNOWN";
+
+ // Ethereum Address and Token (0x followed by 40 hex characters)
+ if (/^0x[a-f0-9]{40}$/.test(cleanInput)) {
+ return "ADDRESS";
+ }
+
+ // Transaction Hash (0x followed by 64 hex characters)
+ if (/^0x[a-f0-9]{64}$/.test(cleanInput)) {
+ return "TRANSACTION-HASH";
+ }
+
+ // Block Number (numeric only)
+ if (/^\d+$/.test(cleanInput)) {
+ return "BLOCK";
+ }
+
+ // Neo4j identifier (at least 3 characters)
+ if (/^0x[a-f0-9]{40}$/.test(cleanInput)) {
+ return "NEO4J";
+ }
+
+ return "UNKNOWN";
+ };
+
+ const handleUniversalSearch = async (input: string) => {
+ const inputType = detectInputType(input);
+
+ switch (inputType) {
+ case "ADDRESS":
+ // Check if it's a token contract
+ const isToken = false; // You would need to implement token detection logic here
+ if (isToken) {
+ return `/token/?address=${encodeURIComponent(input)}`;
+ }
+ return `/search/?address=${encodeURIComponent(input)}&network=mainnet&provider=etherscan`;
+
+ case "TRANSACTION-HASH":
+ return `/txn-hash/?hash=${encodeURIComponent(input)}`;
+
+ case "BLOCK":
+ return `/block/?number=${encodeURIComponent(input)}`;
+
+ case "NEO4J":
+ return `/search-offchain/?address=${encodeURIComponent(input)}`;
+
+ default:
+ throw new Error("Unable to determine search type");
+ }
+ };
+
// Validate Ethereum address
const validateAddress = (addr: string): boolean => {
if (!addr) return false;
@@ -44,9 +100,32 @@ export default function SearchBar() {
setAddressError("Invalid Ethereum address format. Must start with 0x followed by 40 hex characters.");
return false;
}
- } else {
+ } else if(searchType === "Txn Hash"){
+ if(addr.length !== 66){
+ setAddressError("Invalid Transaction Hash format. Must be 66 characters long.");
+ return false;
+ }
+ }else if(searchType === "Token"){
+ if(addr.length !== 42){
+ setAddressError("Invalid Token address format. Must be 42 characters long.");
+ return false;
+ }
+ }else if (searchType === "Block"){
+ if(addr.length < 1){
+ setAddressError("Invalid Block number format. Must be at least 1 character long.");
+ return false;
+ }
+ }else if(searchType === "All"){
+ // Detect logic to search for all types
+ const inputType = detectInputType(addr);
+ if (inputType === "UNKNOWN") {
+ setAddressError("Invalid search input. Please enter a valid address, transaction hash, token address, or block number.");
+ return false;
+ }
+ }
+ else {
// For off-chain searches, validate Neo4j ID format
- if (addr.length < 3) {
+ if (!ETH_ADDRESS_REGEX.test(addr)) {
setAddressError("Neo4j identifier must be at least 3 characters");
return false;
}
@@ -109,7 +188,17 @@ export default function SearchBar() {
await new Promise(resolve => setTimeout(resolve, 1000))
if (searchType === "onchain") {
router.push(`/search/?address=${encodeURIComponent(address)}&network=${network}&provider=${provider}`)
- } else {
+ } else if(searchType === "Txn Hash"){
+ router.push(`/txn-hash/?hash=${encodeURIComponent(address)}`)
+ } else if(searchType === "Token"){
+ router.push(`/token/?address=${encodeURIComponent(address)}`)
+ } else if(searchType === "Block"){
+ router.push(`/block/?number=${encodeURIComponent(address)}`)
+ }else if(searchType === "All"){
+ // Detect logic to search for all types
+ const route = await handleUniversalSearch(address);
+ router.push(route);
+ }else {
router.push(`/search-offchain/?address=${encodeURIComponent(address)}`)
}
} catch (error) {
@@ -147,15 +236,35 @@ export default function SearchBar() {
@@ -181,20 +290,40 @@ export default function SearchBar() {
@@ -239,19 +368,48 @@ export default function SearchBar() {
>
- ) : (
+ ) : searchType === "offchain" ?(
Neo4j Graph Database
- )}
+ ): searchType === "Txn Hash" ? (
+
+
+ Transaction Explorer
+
+ ) : searchType === "Token" ? (
+
+
+ Token Explorer
+
+ ) : searchType === "Block" ? (
+
+
+ Block Explorer
+
+ ) : (
+
+
+ Universal Search
+
+ )
+ }