diff --git a/README.md b/README.md index a64d7ac..28024fe 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ npm install next --legacy-peer-deps touch .env.local ``` Populate `.env.local` with: -``` +```dotenv ETHERSCAN_API_KEY=YOUR_API_KEY ETHERSCAN_API_URL=https://api.etherscan.io/api SMTP_HOST=smtp.example.com @@ -33,6 +33,13 @@ SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=your-email@example.com 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 diff --git a/app/NFT/page.tsx b/app/NFT/page.tsx index 317c4e9..85a4c6e 100644 --- a/app/NFT/page.tsx +++ b/app/NFT/page.tsx @@ -5,6 +5,8 @@ import NFTCard from '@/components/NFT/NFTCard'; import NFTTabs from '@/components/NFT/NFTTabs'; import Pagination from '@/components/NFT/Pagination'; import { useWallet } from '@/components/Faucet/walletcontext'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import ParticlesBackground from '@/components/ParticlesBackground'; // Contract addresses const NFT_CONTRACT_ADDRESS = "0x279Bd9304152E0349427c4B7F35FffFD439edcFa"; @@ -156,6 +158,24 @@ export default function NFTMarketplace() { } }, [account]); + useEffect(() => { + if (account) { + fetchNFTs(); + const interval = setInterval(fetchNFTs, 15000); + return () => clearInterval(interval); + } + }, [account, fetchNFTs]); + + useEffect(() => { + setCurrentPage(1); + }, [activeTab]); + + useEffect(() => { + if (account) { + fetchPathBalance(account); + } + }, [account, fetchPathBalance]); + // Pagination const paginatedData = useMemo(() => { const start = (currentPage - 1) * ITEMS_PER_PAGE; @@ -282,142 +302,129 @@ export default function NFTMarketplace() { } }; - // Effects - useEffect(() => { - if (account) { - fetchNFTs(); - const interval = setInterval(fetchNFTs, 15000); - return () => clearInterval(interval); - } - }, [account, fetchNFTs]); - - useEffect(() => { - setCurrentPage(1); - }, [activeTab]); - - useEffect(() => { - if (account) { - fetchPathBalance(account); - } - }, [account, fetchPathBalance]); - return ( -
- {/* Background effects */} - -
-

- NFT Marketplace -

-
- {account && ( -
- - {pathBalance} - - PATH -
- )} - -
-
- - - - {!account ? ( -
- Please connect your wallet to view NFTs -
- ) : ( - <> - {isInitialLoad ? ( -
- {[...Array(8)].map((_, i) => ( -
- ))} -
- ) : ( - <> + +
+ + {/* End updated header section */} + + + + {!account ? ( +
+ Please connect your wallet to view NFTs +
+ ) : ( + <> + {isInitialLoad ? (
- {paginatedData.map((nft, index) => ( + {[...Array(8)].map((_, i) => (
- handleBuyNFT(tokenId, price || '0') : - activeTab === 'owned' ? (tokenId, price) => handleListNFT(tokenId, price || '0') : - handleUnlistNFT - } - processing={processing} - /> -
+ key={i} + className="animate-pulse bg-black rounded-xl h-[500px] shadow-lg" + /> ))}
+ ) : ( + <> +
+ {paginatedData.map((nft, index) => ( +
+ handleBuyNFT(tokenId, price || '0') + : activeTab === 'owned' + ? (tokenId, price) => handleListNFT(tokenId, price || '0') + : handleUnlistNFT + } + processing={processing} + /> +
+ ))} +
- {totalPages > 1 && ( -
- 1 && ( +
+ +
+ )} + + )} + + )} + + {processing && ( +
+
+
+

Processing Transaction

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

Processing Transaction

-
- {[...Array(3)].map((_, i) => ( -
- ))} + ))} +
-
- )} + )} +
); -} \ No newline at end of file +} diff --git a/app/api/dappradar-trending-nft/route.ts b/app/api/dappradar-trending-nft/route.ts new file mode 100644 index 0000000..e82ef26 --- /dev/null +++ b/app/api/dappradar-trending-nft/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server'; + + export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const chain = searchParams.get('chain') || 'ethereum'; + const API_KEY = process.env.REACT_APP_DAPPRADAR_API_KEY; + + try { + const nftResponse = await fetch(`https://apis.dappradar.com/v2/nfts/collections?range=24h&sort=sales&order=desc&chain=${chain}&resultsPerPage=10`, { + headers: { + accept: 'application/json', + 'x-api-key': API_KEY || '', + }, + } + ); + + + // Check if any request failed. + if (!nftResponse.ok) { + return NextResponse.json( + { + error: `Returned an error: dapps(${nftResponse.status}))`, + }, + { status: 404 } + ); + } + + const nftData = await nftResponse.json(); + + return NextResponse.json({ + nfts: nftData, + }); + } catch (error: any) { + return NextResponse.json( + { error: error.message || 'Internal server error' }, + { status: 500 } + ); + } + } diff --git a/app/api/dappradar-trending-project/route.ts b/app/api/dappradar-trending-project/route.ts new file mode 100644 index 0000000..16dd702 --- /dev/null +++ b/app/api/dappradar-trending-project/route.ts @@ -0,0 +1,56 @@ +// api/dappradar/route.ts +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const chain = searchParams.get('chain') || 'ethereum'; + const API_KEY = process.env.REACT_APP_DAPPRADAR_API_KEY; + + try { + const [dappsResponse, gamesResponse, marketplaceResponse] = await Promise.all([ + fetch(`https://apis.dappradar.com/v2/dapps/top/uaw?chain=${chain}&range=7d&top=10`, { + headers: { + accept: 'application/json', + 'x-api-key': API_KEY || '', + }, + }), + fetch(`https://apis.dappradar.com/v2/dapps/top/balance?chain=${chain}&category=games&range=24h&top=10`, { + headers: { + accept: 'application/json', + 'x-api-key': API_KEY || '', + }, + }), + fetch(`https://apis.dappradar.com/v2/dapps/top/transactions?chain=${chain}&category=marketplaces&range=24h&top=10`, { + headers: { + accept: 'application/json', + 'x-api-key': API_KEY || '', + }, + }), + ]); + + // Check if any request failed. + if (!dappsResponse.ok || !gamesResponse.ok || !marketplaceResponse.ok) { + return NextResponse.json( + { + error: `One or more endpoints returned an error: dapps(${dappsResponse.status}), game(${gamesResponse.status}), arketplace(${marketplaceResponse.status})`, + }, + { status: 404 } + ); + } + + const dappsData = await dappsResponse.json(); + const gamesData = await gamesResponse.json(); + const marketplacesData = await marketplaceResponse.json(); + + return NextResponse.json({ + dapps: dappsData, + games: gamesData, + marketplaces: marketplacesData, + }); + } catch (error: any) { + return NextResponse.json( + { error: error.message || 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/etherscan/route.ts b/app/api/etherscan/route.ts index 7d8bac3..2a46e51 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 } + ) } -} +} \ No newline at end of file diff --git a/app/api/infura/route.ts b/app/api/infura/route.ts new file mode 100644 index 0000000..47547da --- /dev/null +++ b/app/api/infura/route.ts @@ -0,0 +1,109 @@ +import { NextResponse } from "next/server"; + +const INFURA_API_KEY = process.env.NEXT_PUBLIC_INFURA_KEY; // Will be moved to env process later + +const networkUrls: { mainnet: string; optimism: string; arbitrum: string; } = { + mainnet: `https://mainnet.infura.io/v3/${INFURA_API_KEY}`, + optimism: `https://optimism-mainnet.infura.io/v3/${INFURA_API_KEY}`, + arbitrum: `https://arbitrum-mainnet.infura.io/v3/${INFURA_API_KEY}` +}; + +// Define the network type first +type Network = 'mainnet' | 'optimism' | 'arbitrum'; + +// Simple in-memory cache +const cache = new Map(); +const CACHE_DURATION = 10000; // 10 seconds cache (increased from 5s) +let lastCallTimestamp = 0; +const RATE_LIMIT_WINDOW = 300; // 300ms between calls (reduced from 5 calls to 3-4 calls per second) + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { method, params, network = "mainnet" } = body; + + // Create cache key from the method and params + const cacheKey = `${network}-${method}-${JSON.stringify(params)}`; + + // 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) { + const waitTime = RATE_LIMIT_WINDOW - (now - lastCallTimestamp); + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + lastCallTimestamp = Date.now(); + + // Select network URL + const networkUrl = networkUrls[network as Network] || networkUrls.mainnet; + + // Prepare JSON-RPC request + const rpcRequest = { + jsonrpc: "2.0", + id: 1, + method, + params + }; + + // Call Infura API with NO timeout - let the browser or network naturally timeout + console.log(`Making Infura request: ${method} to ${network}`); + + try { + const response = await fetch(networkUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(rpcRequest) + }); + + if (!response.ok) { + throw new Error(`Infura API responded with status: ${response.status}`); + } + + // First check if the response is JSON + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + throw new Error("Infura API returned a non-JSON response"); + } + + const data = await response.json(); + + // Verify that the data is valid JSON-RPC + if (!data || typeof data !== 'object' || (!data.result && !data.error)) { + throw new Error("Invalid JSON-RPC response from Infura"); + } + + // Cache the successful response + cache.set(cacheKey, { data, timestamp: Date.now() }); + + console.log(`Infura request successful: ${method}`); + return NextResponse.json(data); + } catch (error) { + console.error(`Infura request error for ${method}:`, error); + throw error; + } + } catch (error) { + console.error('Infura API error:', error); + const errorMessage = error instanceof Error + ? error.message + : "Failed to fetch from Infura"; + + return NextResponse.json( + { + jsonrpc: "2.0", + id: 1, + error: { + code: -32603, + message: errorMessage + } + }, + { status: 500 } + ); + } +} diff --git a/app/api/nfts/route.ts b/app/api/nfts/route.ts index a5b549e..cb6ea8e 100644 --- a/app/api/nfts/route.ts +++ b/app/api/nfts/route.ts @@ -1,61 +1,121 @@ import { NextResponse } from "next/server" -const ETHERSCAN_API_URL = "https://api.etherscan.io/api" +const ALCHEMY_API_URL = "https://eth-mainnet.g.alchemy.com/nft/v2" +const DEFAULT_IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs/" interface NFT { tokenID: string tokenName: string tokenSymbol: string contractAddress: string + imageUrl: string } -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - const address = searchParams.get("address") +interface NFTResponse { + nfts: NFT[] + totalCount: number + pageKey?: string | null + error?: string +} - if (!address) { - return NextResponse.json({ error: "Address is required" }, { status: 400 }) +const convertIpfsToGatewayUrl = (url: string): string => { + if (!url) return "/images/nft-placeholder.png" + + // Handle ipfs:// protocol + if (url.startsWith("ipfs://")) { + const cidAndPath = url.replace("ipfs://", "") + return `${DEFAULT_IPFS_GATEWAY}${cidAndPath}` } + + // Already using a gateway, return as is + if (url.includes("ipfs") && + (url.startsWith("http://") || url.startsWith("https://"))) { + return url + } + + return url +} - try { - const response = await fetch( - `${ETHERSCAN_API_URL}?module=account&action=tokennfttx&address=${address}&startblock=0&endblock=99999999&sort=desc&apikey=${process.env.ETHERSCAN_API_KEY}`, - ) +// Enhanced address validation +const isValidAddress = (address: string): boolean => { + // Basic Ethereum address format check + return /^0x[a-fA-F0-9]{40}$/.test(address); +}; +export async function GET(request: Request) { + try { + // Parse search parameters + const { searchParams } = new URL(request.url) + const address = searchParams.get("address") + const limit = Number(searchParams.get("limit") || "15") + const pageKey = searchParams.get("pageKey") // Extract pageKey from the request + + // Validate address + if (!address) { + return NextResponse.json({ + error: "Address parameter is required", + }, { status: 400 }); + } + + // Validate address format + if (!isValidAddress(address)) { + return NextResponse.json({ + error: `"${address}" is not a valid Ethereum address`, + details: "Address must start with 0x followed by 40 hexadecimal characters.", + invalidAddress: address + }, { status: 400 }); + } + + // Fetch NFTs from API with pagination + const apiKey = process.env.ALCHEMY_API_KEY + const baseURL = `https://eth-mainnet.g.alchemy.com/nft/v2/${apiKey}/getNFTs` + + // Build URL with pagination parameters + let url = `${baseURL}?owner=${address}&withMetadata=true&pageSize=${limit}` + + // Add pageKey if provided for pagination + if (pageKey) { + url += `&pageKey=${pageKey}` + } + + console.log(`Fetching NFTs with URL: ${url.replace(apiKey || "", "[REDACTED]")}`) + + const response = await fetch(url, { + headers: { Accept: "application/json" } + }) + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() - if (data.status !== "1") { - // If no NFTs are found, return an empty array instead of throwing an error - if (data.message === "No transactions found") { - return NextResponse.json([]) - } - throw new Error(data.message || "Etherscan API returned an error") + if (!data.ownedNfts || data.ownedNfts.length === 0) { + return NextResponse.json({ + nfts: [], + totalCount: 0, + pageKey: null + }, { status: 200 }) } - const nfts = data.result.reduce((acc: NFT[], tx: any) => { - const existingNFT = acc.find((nft) => nft.contractAddress === tx.contractAddress && nft.tokenID === tx.tokenID) - if (!existingNFT) { - acc.push({ - tokenID: tx.tokenID, - tokenName: tx.tokenName, - tokenSymbol: tx.tokenSymbol, - contractAddress: tx.contractAddress, - }) - } - return acc - }, []) - - return NextResponse.json(nfts) + const nfts = data.ownedNfts.map((nft: any) => ({ + tokenID: nft.id.tokenId, + tokenName: nft.metadata?.name || "Unnamed NFT", + tokenSymbol: nft.contract?.symbol || "", + contractAddress: nft.contract?.address, + imageUrl: convertIpfsToGatewayUrl(nft.metadata?.image || nft.metadata?.image_url || "/images/nft-placeholder.png") + })) + + return NextResponse.json({ + nfts, + totalCount: data.totalCount || nfts.length, + pageKey: data.pageKey || null // Ensure we're passing the next pageKey back + }, { status: 200 }) } catch (error) { console.error("Error fetching NFTs:", error) return NextResponse.json( - { error: error instanceof Error ? error.message : "An unknown error occurred" }, - { status: 500 }, + { error: error instanceof Error ? error.message : "Failed to fetch NFTs" }, + { status: 500 } ) } -} - +} \ No newline at end of file diff --git a/app/api/pending/route.ts b/app/api/pending/route.ts new file mode 100644 index 0000000..8d3419e --- /dev/null +++ b/app/api/pending/route.ts @@ -0,0 +1,69 @@ + +import { NextResponse } from "next/server" + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const network = searchParams.get("network") || "mainnet" + + try { + // Use window.location.origin as fallback when process.env.NEXT_PUBLIC_URL is undefined + const baseUrl = typeof window !== 'undefined' + ? window.location.origin + : process.env.NEXT_PUBLIC_URL || '' + + // For Ethereum mainnet, use Etherscan API + if (network === 'mainnet') { + const etherscanResponse = await fetch( + `${baseUrl}/api/etherscan?module=proxy&action=eth_getBlockTransactionCountByNumber&tag=pending` + ); + + if (!etherscanResponse.ok) { + throw new Error(`HTTP error! status: ${etherscanResponse.status}`) + } + + const etherscanData = await etherscanResponse.json(); + + if (etherscanData.error) { + throw new Error(etherscanData.error || "Etherscan API returned an error") + } + + const pendingTxCount = parseInt(etherscanData.result, 16); + + return NextResponse.json({ pendingTransactions: pendingTxCount, network }); + } + // For other networks, use Infura API + else { + const response = await fetch(`${baseUrl}/api/infura`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + method: "eth_getBlockTransactionCountByNumber", + params: ["pending"], + network + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + if (data.error) { + throw new Error(data.error.message || "Infura API returned an error") + } + + const pendingTxCount = parseInt(data.result, 16) + + return NextResponse.json({ pendingTransactions: pendingTxCount, network }) + } + } catch (error) { + console.error("Error fetching pending transactions:", error) + return NextResponse.json( + { error: error instanceof Error ? error.message : "An unknown error occurred" }, + { status: 500 } + ) + } +} diff --git a/app/api/portfolio/route.ts b/app/api/portfolio/route.ts new file mode 100644 index 0000000..85e2e95 --- /dev/null +++ b/app/api/portfolio/route.ts @@ -0,0 +1,108 @@ +import { NextResponse } from "next/server"; +import * as moralisApi from "@/lib/api/moralisApi"; +import * as alchemyApi from "@/lib/api/alchemyApi"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const provider = searchParams.get("provider") || "moralis"; // Default to Moralis + + if (!address) { + return NextResponse.json( + { error: "Address is required" }, + { status: 400 } + ); + } + + // Validate Ethereum address format + if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { + return NextResponse.json( + { error: "Invalid Ethereum address format" }, + { status: 400 } + ); + } + + interface Token { + contractAddress?: string; + chain?: string; + balanceFormatted?: string; + usdPrice?: number; + usdValue?: number; + [key: string]: any; // Allow for other properties + } + + let tokens: Token[] = []; + let totalValue = 0; + let apiError = null; + + // Fetch tokens based on provider + try { + if (provider === "moralis") { + tokens = await moralisApi.getWalletTokens(address); + + // Check if we got tokens but Moralis API key isn't configured + if (tokens.length === 0 && !process.env.MORALIS_API_KEY) { + apiError = "Moralis API key is not configured. Please set MORALIS_API_KEY in your environment variables."; + } + } else if (provider === "alchemy") { + tokens = await alchemyApi.getWalletTokens(address); + + // Enrich Alchemy tokens with price data + const nativePrices = await alchemyApi.getNativePrices(); + + tokens = tokens.map(token => { + if (token.contractAddress === 'native') { + const usdPrice = token.chain ? nativePrices[token.chain] || 0 : 0; + const balance = parseFloat(token.balanceFormatted || '0'); + return { + ...token, + usdPrice, + usdValue: balance * usdPrice + }; + } + return token; + }); + + // For ERC20 tokens, we would need to fetch prices from another source + // This is a simplified version + } else if (provider === "combined") { + // Fetch from both providers and merge results + const moralisTokens = await moralisApi.getWalletTokens(address); + const alchemyTokens = await alchemyApi.getWalletTokens(address); + + // In a real implementation, you would deduplicate tokens here + tokens = [...moralisTokens, ...alchemyTokens]; + } else { + return NextResponse.json( + { error: "Invalid provider. Use 'moralis', 'alchemy', or 'combined'" }, + { status: 400 } + ); + } + } catch (providerError) { + console.error(`Error with ${provider} provider:`, providerError); + apiError = `The ${provider} provider encountered an error: ${ + providerError instanceof Error ? providerError.message : String(providerError) + }`; + } + + // Calculate total USD value + totalValue = tokens.reduce((acc, token) => acc + (token.usdValue || 0), 0); + + return NextResponse.json({ + address, + tokens, + totalValue, + totalBalance: tokens.length, + provider, + apiError // Include any API-specific errors in the response + }); + } catch (error) { + console.error("Error fetching portfolio:", error); + + return NextResponse.json( + { error: "Failed to fetch portfolio data", details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} diff --git a/app/api/token-transactions/route.ts b/app/api/token-transactions/route.ts new file mode 100644 index 0000000..4a375fd --- /dev/null +++ b/app/api/token-transactions/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from "next/server"; +import { TOKEN_CONTRACTS } from "@/services/cryptoService"; + +const ETHERSCAN_API_URL = "https://api.etherscan.io/api"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const coinId = searchParams.get("coinId"); + const page = searchParams.get("page") || "1"; + const offset = searchParams.get("offset") || "50"; + + if (!coinId) { + return NextResponse.json({ error: "Coin ID is required" }, { status: 400 }); + } + + try { + const contractAddress = TOKEN_CONTRACTS[coinId]; + if (!contractAddress) { + return NextResponse.json({ error: "Unsupported token" }, { status: 400 }); + } + + let url: string; + if (coinId === 'ethereum') { + // For ETH, fetch normal transactions + url = `${ETHERSCAN_API_URL}?module=account&action=txlist&address=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D&startblock=0&endblock=99999999&page=${page}&offset=${offset}&sort=desc&apikey=${process.env.ETHERSCAN_API_KEY}`; + } else { + // For other tokens, fetch their specific ERC20 transactions + url = `${ETHERSCAN_API_URL}?module=account&action=tokentx&contractaddress=${contractAddress}&page=${page}&offset=${offset}&sort=desc&apikey=${process.env.ETHERSCAN_API_KEY}`; + } + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (data.status !== "1") { + if (data.message === "No transactions found") { + return NextResponse.json([]); + } + throw new Error(data.message || "Etherscan API returned an error"); + } + + const transactions = data.result.map((tx: any) => ({ + hash: tx.hash, + from: tx.from, + to: tx.to, + value: coinId === 'ethereum' + ? `${(Number(tx.value) / 1e18).toFixed(6)} ETH` + : `${(Number(tx.value) / Math.pow(10, Number(tx.tokenDecimal))).toFixed(6)} ${tx.tokenSymbol || coinId.toUpperCase()}`, + timestamp: Number(tx.timeStamp) * 1000, + gasPrice: tx.gasPrice, + gasUsed: tx.gasUsed, + method: tx.input && tx.input !== '0x' ? 'Contract Interaction' : 'Transfer' + })); + + return NextResponse.json(transactions); + } catch (error) { + console.error("Error fetching token 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/api/transactions/route.ts b/app/api/transactions/route.ts index 79c2f79..204203b 100644 --- a/app/api/transactions/route.ts +++ b/app/api/transactions/route.ts @@ -1,46 +1,274 @@ import { NextResponse } from "next/server" -const ETHERSCAN_API_URL = "https://api.etherscan.io/api" +// Add timeout utility - only for non-Infura requests +const fetchWithTimeout = async (url: string, options: RequestInit = {}, timeout = 15000, isInfura = false) => { + if (isInfura) { + // For Infura, don't use a timeout + return fetch(url, options); + } + + const controller = new AbortController(); + const { signal } = controller; + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + controller.abort(); + reject(new Error("Request timeout")); + }, timeout); + }); + + return Promise.race([ + fetch(url, { ...options, signal }), + timeoutPromise + ]); +}; + +// Enhanced address validation +const isValidAddress = (address: string): boolean => { + // Basic Ethereum address format check + return /^0x[a-fA-F0-9]{40}$/.test(address); +}; export async function GET(request: Request) { const { searchParams } = new URL(request.url) const address = searchParams.get("address") - const page = searchParams.get("page") || "1" - const offset = searchParams.get("offset") || "50" // Increased to 50 transactions + const network = searchParams.get("network") || "mainnet" + const provider = searchParams.get("provider") || "etherscan" + const page = parseInt(searchParams.get("page") || "1") + const offset = parseInt(searchParams.get("offset") || "20") + // Validate address presence if (!address) { - return NextResponse.json({ error: "Address is required" }, { status: 400 }) + return NextResponse.json({ + error: "Address is required", + details: "Please provide an Ethereum address parameter." + }, { status: 400 }) + } + + // Validate address format + if (!isValidAddress(address)) { + return NextResponse.json({ + error: `"${address}" is not a valid Ethereum address`, + details: "Address must start with 0x followed by 40 hexadecimal characters.", + invalidAddress: address, + suggestion: "Check for typos and ensure you have copied the complete address." + }, { status: 400 }); } try { - const response = await fetch( - `${ETHERSCAN_API_URL}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=${page}&offset=${offset}&sort=desc&apikey=${process.env.ETHERSCAN_API_KEY}`, - ) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) + // Get the base URL dynamically + const baseUrl = typeof window !== 'undefined' + ? window.location.origin + : process.env.NEXT_PUBLIC_URL || '' + + // Use Etherscan API for Ethereum mainnet + if (provider === 'etherscan' && network === 'mainnet') { + const etherscanResponse = await fetchWithTimeout( + `${baseUrl}/api/etherscan?module=account&action=txlist&address=${address}&page=${page}&offset=${offset}&sort=desc`, + {}, + 20000 // 20s timeout + ) + + if (!etherscanResponse.ok) { + throw new Error(`Etherscan API responded with status: ${etherscanResponse.status}`) + } + + const etherscanData = await etherscanResponse.json() + + if (etherscanData.status !== "1") { + // If no transactions are found, return an empty array + if (etherscanData.message === "No transactions found") { + return NextResponse.json([]) + } + throw new Error(etherscanData.message || "Etherscan API returned an error") + } + + // Transform Etherscan data to match our expected format + const transactions = etherscanData.result.map((tx: any) => { + const valueInEth = parseInt(tx.value) / 1e18 + + return { + id: tx.hash, + from: tx.from, + to: tx.to || "Contract Creation", + value: `${valueInEth.toFixed(4)} ETH`, + timestamp: new Date(parseInt(tx.timeStamp) * 1000).toISOString(), + network, + gas: parseInt(tx.gas), + gasPrice: parseInt(tx.gasPrice) / 1e9, // in gwei + blockNumber: parseInt(tx.blockNumber), + nonce: parseInt(tx.nonce) + } + }) + + return NextResponse.json(transactions) } - - const data = await response.json() - - if (data.status !== "1") { - throw new Error(data.message || "Etherscan API returned an error") + // Use Infura for other networks (Optimism, Arbitrum) + else if (provider === 'infura') { + console.log(`Infura transaction search started for ${address} on ${network}`); + // First get the latest block number - no timeout for Infura + const blockNumberResponse = await fetchWithTimeout( + `${baseUrl}/api/infura`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + method: "eth_blockNumber", + params: [], + network + }), + }, + 0, // No timeout for Infura + true // Mark as Infura request + ); + + if (!blockNumberResponse.ok) { + throw new Error(`Infura API responded with status: ${blockNumberResponse.status}`); + } + + const blockNumberData = await blockNumberResponse.json(); + + if (blockNumberData.error) { + throw new Error(blockNumberData.error.message || "Failed to fetch block number"); + } + + const latestBlock = parseInt(blockNumberData.result, 16); + console.log(`Latest block number: ${latestBlock}`); + + // Use a more direct approach for getting transactions + // Just scan the most recent N blocks for transactions involving the address + const blockRange = 50; // Reduced block range to prevent timeouts + const transactions = []; + const processedTxHashes = new Set(); + + // Determine currency symbol based on network + const currencySymbol = network === "optimism" ? "ETH" : + network === "arbitrum" ? "ETH" : "ETH"; + + console.log(`Scanning ${blockRange} recent blocks for transactions...`); + + // Start from the latest block and work backward + for (let blockNum = latestBlock; blockNum > latestBlock - blockRange; blockNum--) { + try { + console.log(`Processing block ${blockNum}...`); + // Get the block with transactions - no timeout for Infura + const blockResponse = await fetchWithTimeout( + `${baseUrl}/api/infura`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + method: "eth_getBlockByNumber", + params: [`0x${blockNum.toString(16)}`, true], + network + }), + }, + 0, // No timeout for Infura + true // Mark as Infura request + ); + + // Check for valid response + if (!blockResponse.ok) { + console.warn(`Skipping block ${blockNum} due to non-OK response`); + continue; // Skip if we can't get this block + } + + const blockData = await blockResponse.json(); + + // Skip if no data or no transactions + if (blockData.error || !blockData.result || !blockData.result.transactions) { + console.warn(`Skipping block ${blockNum} due to missing data`, blockData.error || 'No transactions'); + continue; + } + + // Filter transactions for the address + const addrLower = address.toLowerCase(); + const relevantTxs = blockData.result.transactions.filter( + (tx: any) => + tx.from && tx.from.toLowerCase() === addrLower || + tx.to && tx.to.toLowerCase() === addrLower + ); + + console.log(`Found ${relevantTxs.length} relevant transactions in block ${blockNum}`); + + // For each relevant transaction, get more details + for (const tx of relevantTxs) { + // Skip if we already processed this transaction + if (processedTxHashes.has(tx.hash)) { + continue; + } + + const valueInWei = tx.value ? parseInt(tx.value, 16) : 0; + + transactions.push({ + id: tx.hash, + from: tx.from, + to: tx.to || "Contract Creation", + value: `${(valueInWei / 1e18).toFixed(4)} ${currencySymbol}`, + timestamp: new Date(parseInt(blockData.result.timestamp, 16) * 1000).toISOString(), + network, + gas: parseInt(tx.gas, 16), + gasPrice: tx.gasPrice ? parseInt(tx.gasPrice, 16) / 1e9 : 0, // in gwei + blockNumber: parseInt(tx.blockNumber, 16), + nonce: parseInt(tx.nonce, 16) + }); + + processedTxHashes.add(tx.hash); + } + + // Stop if we've collected enough transactions for this page + if (transactions.length >= offset) { + console.log(`Collected enough transactions (${transactions.length}), stopping block scan`); + break; + } + } catch (blockError) { + console.warn(`Error fetching block ${blockNum}:`, blockError); + continue; // Skip problematic blocks + } + } + + console.log(`Transaction scan complete, found ${transactions.length} transactions`); + + // Sort by block number (timestamp) descending + transactions.sort((a, b) => b.blockNumber - a.blockNumber); + + // Apply pagination + const paginatedTransactions = transactions.slice( + Math.min((page - 1) * offset, transactions.length), + Math.min(page * offset, transactions.length) + ); + + return NextResponse.json(paginatedTransactions.length > 0 ? paginatedTransactions : []); + } + else { + return NextResponse.json({ error: "Unsupported provider or network combination" }, { status: 400 }); } - - const transactions = data.result.map((tx: any) => ({ - id: tx.hash, - from: tx.from, - to: tx.to, - value: `${(Number.parseFloat(tx.value) / 1e18).toFixed(4)} ETH`, - timestamp: new Date(Number.parseInt(tx.timeStamp) * 1000).toISOString(), - })) - - return NextResponse.json(transactions) } catch (error) { - console.error("Error fetching transactions:", error) + console.error("Error fetching transactions:", error); + let errorMessage = "Failed to fetch transactions"; + let statusCode = 500; + + if (error instanceof Error) { + if (error.name === "AbortError") { + errorMessage = "Request was aborted while fetching transactions. Please try using Infura provider which has no timeout."; + statusCode = 499; // Client Closed Request + } else if (error.message === "Request timeout") { + errorMessage = `Request timed out while fetching transactions. ${ + provider === 'infura' ? 'Please try again.' : 'Try switching to Infura provider.' + }`; + statusCode = 504; // Gateway Timeout + } else { + errorMessage = error.message; + } + } + return NextResponse.json( - { error: error instanceof Error ? error.message : "An unknown error occurred" }, - { status: 500 }, - ) + { error: errorMessage }, + { status: statusCode }, + ); } -} \ No newline at end of file +} diff --git a/app/api/wallet/route.ts b/app/api/wallet/route.ts index d63cf96..26ade44 100644 --- a/app/api/wallet/route.ts +++ b/app/api/wallet/route.ts @@ -1,37 +1,228 @@ import { NextResponse } from "next/server" -const ETHERSCAN_API_URL = "https://api.etherscan.io/api" +// Add a timeout function for fetch requests - skip timeout for Infura +const fetchWithTimeout = async (url: string, options: RequestInit = {}, timeout = 15000, isInfura = false) => { + if (isInfura) { + // For Infura, don't use a timeout + return fetch(url, options); + } + + const controller = new AbortController(); + const { signal } = controller; + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + controller.abort(); + reject(new Error("Request timeout")); + }, timeout); + }); + + return Promise.race([ + fetch(url, { ...options, signal }), + timeoutPromise + ]); +}; + +// Enhanced address validation +const isValidAddress = (address: string): boolean => { + // Basic Ethereum address format check + return /^0x[a-fA-F0-9]{40}$/.test(address); +}; export async function GET(request: Request) { const { searchParams } = new URL(request.url) const address = searchParams.get("address") + const network = searchParams.get("network") || "mainnet" + const provider = searchParams.get("provider") || "etherscan" + // Enhanced input validation if (!address) { - return NextResponse.json({ error: "Address is required" }, { status: 400 }) + return NextResponse.json({ + error: "Address is required", + details: "Please provide an Ethereum address parameter." + }, { status: 400 }) + } + + // Validate address format - return helpful error details + if (!isValidAddress(address)) { + return NextResponse.json({ + error: `"${address}" is not a valid Ethereum address`, + details: "Address must start with 0x followed by 40 hexadecimal characters.", + invalidAddress: address, + suggestion: "Check for typos and ensure you have copied the complete address." + }, { status: 400 }); } try { - const balanceResponse = await fetch( - `${ETHERSCAN_API_URL}?module=account&action=balance&address=${address}&tag=latest&apikey=${process.env.ETHERSCAN_API_KEY}`, - ) - const balanceData = await balanceResponse.json() - - const txCountResponse = await fetch( - `${ETHERSCAN_API_URL}?module=proxy&action=eth_getTransactionCount&address=${address}&tag=latest&apikey=${process.env.ETHERSCAN_API_KEY}`, - ) - const txCountData = await txCountResponse.json() - - const balance = Number.parseFloat(balanceData.result) / 1e18 // Convert wei to ETH - const transactionCount = Number.parseInt(txCountData.result, 16) // Convert hex to decimal - - return NextResponse.json({ - address, - balance: `${balance.toFixed(4)} ETH`, - transactionCount, - }) + // Get the base URL dynamically + const baseUrl = typeof window !== 'undefined' + ? window.location.origin + : process.env.NEXT_PUBLIC_URL || '' + + // Use Etherscan for Ethereum Mainnet + if (provider === 'etherscan' && network === 'mainnet') { + // Fetch balance using Etherscan with timeout + try { + const etherscanBalanceResponse = await fetchWithTimeout( + `${baseUrl}/api/etherscan?module=account&action=balance&address=${address}&tag=latest`, + {}, + 15000 // 15s timeout (increased from 10s) + ); + + if (!etherscanBalanceResponse.ok) { + throw new Error(`Etherscan API responded with status: ${etherscanBalanceResponse.status}`); + } + + const balanceData = await etherscanBalanceResponse.json(); + + if (balanceData.status !== "1") { + // Special handling for "No transactions found" which is not an actual error + if (balanceData.message === "No transactions found") { + // Return empty balance instead of error + const balance = 0; + return NextResponse.json({ + address, + balance: `0.0000 ETH`, + transactionCount: 0, + network + }); + } + throw new Error(balanceData.message || "Failed to fetch balance from Etherscan"); + } + + // Fetch transaction count using Etherscan + const etherscanTxCountResponse = await fetchWithTimeout( + `${baseUrl}/api/etherscan?module=proxy&action=eth_getTransactionCount&address=${address}&tag=latest`, + {}, + 15000 // 15s timeout (increased from 10s) + ); + + if (!etherscanTxCountResponse.ok) { + throw new Error(`Etherscan API responded with status: ${etherscanTxCountResponse.status}`); + } + + const txCountData = await etherscanTxCountResponse.json(); + + if (txCountData.status === "0") { + throw new Error(txCountData.message || "Failed to fetch transaction count from Etherscan"); + } + + const balance = Number.parseInt(balanceData.result, 10) / 1e18; // Convert wei to ETH + const transactionCount = Number.parseInt(txCountData.result, 16); // Convert hex to decimal + + return NextResponse.json({ + address, + balance: `${balance.toFixed(4)} ETH`, + transactionCount, + network + }); + } catch (error) { + console.error("Etherscan API error:", error); + throw error; // Let the outer catch block handle it + } + } + // Use Infura for other networks + else if (provider === 'infura') { + try { + console.log(`Fetching wallet data via Infura for ${address} on ${network}`); + + // Fetch balance using Infura without timeout + const infuraBalanceResponse = await fetchWithTimeout(`${baseUrl}/api/infura`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + method: "eth_getBalance", + params: [address, "latest"], + network + }), + }, 0, true); // No timeout for Infura, mark as Infura request + + if (!infuraBalanceResponse.ok) { + throw new Error(`Infura API responded with status: ${infuraBalanceResponse.status}`); + } + + const balanceData = await infuraBalanceResponse.json(); + + if (balanceData.error) { + throw new Error(balanceData.error.message || "Failed to fetch balance from Infura"); + } + + // Fetch transaction count using Infura without timeout + const infuraTxCountResponse = await fetchWithTimeout(`${baseUrl}/api/infura`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + method: "eth_getTransactionCount", + params: [address, "latest"], + network + }), + }, 0, true); // No timeout for Infura, mark as Infura request + + if (!infuraTxCountResponse.ok) { + throw new Error(`Infura API responded with status: ${infuraTxCountResponse.status}`); + } + + const txCountData = await infuraTxCountResponse.json(); + + if (txCountData.error) { + throw new Error(txCountData.error.message || "Failed to fetch transaction count from Infura"); + } + + // Determine currency symbol based on network + const currencySymbol = network === "optimism" || network === "arbitrum" ? "ETH" : "ETH"; + + // Handle case where balance is zero (return 0 instead of failing) + const hexBalance = balanceData.result || "0x0"; + const balance = Number.parseInt(hexBalance, 16) / 1e18; // Convert wei to ETH + const transactionCount = Number.parseInt(txCountData.result || "0x0", 16); // Convert hex to decimal + + return NextResponse.json({ + address, + balance: `${balance.toFixed(4)} ${currencySymbol}`, + transactionCount, + network + }); + } catch (error) { + console.error("Infura API error:", error); + throw error; // Let the outer catch block handle it + } + } + else { + return NextResponse.json({ error: "Unsupported provider or network combination" }, { status: 400 }); + } } catch (error) { - console.error("Error fetching wallet data:", error) - return NextResponse.json({ error: "Failed to fetch wallet data" }, { status: 500 }) + console.error("Error fetching wallet data:", error); + + // More specific error messages based on the error type + let errorMessage = "Failed to fetch wallet data"; + let statusCode = 500; + + if (error instanceof Error) { + if (error.name === "AbortError") { + errorMessage = provider === 'infura' + ? "Unexpected abort of Infura request. Please try again." + : "Request was aborted. Try switching to Infura provider which has no timeout."; + statusCode = 499; // Client Closed Request + } else if (error.message === "Request timeout") { + errorMessage = provider === 'infura' + ? "Infura request timed out unexpectedly." + : "Request timed out while fetching wallet data. Try switching to Infura."; + statusCode = 504; // Gateway Timeout + } else if (error.message.includes("Failed to fetch")) { + errorMessage = "Network error while fetching wallet data"; + statusCode = 503; // Service Unavailable + } else if (error.message.includes("not found") || error.message.includes("No record")) { + errorMessage = `Address not found on ${network} network`; + statusCode = 404; // Not Found + } else { + errorMessage = error.message; + } + } + + return NextResponse.json({ error: errorMessage }, { status: statusCode }); } } - diff --git a/app/globals.css b/app/globals.css index 9caeb36..3041614 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; } /* ====================================== @@ -220,17 +220,14 @@ /* Legacy navigation styles - consider migrating to cp-nav-link */ nav a { - @apply relative p-4 m-4 transition-all duration-200; + @apply p-4 transition-all duration-200; animation: appear 2s forwards; } - - nav a::after { - @apply content-[""] h-[3px] w-0 bg-white absolute left-0 bottom-0 transition-all duration-500; - } - - nav a:hover::after { - @apply w-full; + nav form{ + @apply transition-all duration-200; + animation: appear 2s forwards; } + /* ====================================== * Mobile Menu diff --git a/app/layout.tsx b/app/layout.tsx index da6081e..d13bdc4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,24 +1,47 @@ +/** + * 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 { 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 +import { DebugBadge } from "@/components/ui/debug-badge"; +import { SettingsProvider } from "@/components/context/SettingsContext"; // Add this import +import SearchOnTop from "@/components/SearchOnTop"; +export const dynamic = 'force-dynamic'; +/** + * 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", @@ -45,26 +68,39 @@ export const metadata: Metadata = { }, }; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +/** + * 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 ( - - - {/* Bao bọc bằng WalletProvider */} - {/* ✅ Bọc bên trong Client Component */} - -
- {children} - -
- - + + + {/* AuthProvider - Manages user authentication state */} + + {/* Add SettingsProvider here */} + + {/* WalletProvider - Manages blockchain wallet connections and state */} + + {/* QueryProvider - Handles data fetching and caching */} + + {/* Application UI components */} + {/* Initial loading screen */} + +
{/* Global navigation */} + {children} {/* Page-specific content */} + {/* Toast notification container */} +
{/* Global footer */} + + {/* Debug Badge - Only shows in development when needed */} + + + + + ); -} \ No newline at end of file +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 9970630..2c3ba0a 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,55 +1,73 @@ -'use client'; -import Link from 'next/link'; -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import ParticlesBackground from '@/components/ParticlesBackground'; -import { Web3OnboardProvider, init, useConnectWallet } from '@web3-onboard/react'; -import injectedModule from '@web3-onboard/injected-wallets'; -import walletConnectModule from '@web3-onboard/walletconnect'; -import coinbaseModule from '@web3-onboard/coinbase'; -import infinityWalletModule from '@web3-onboard/infinity-wallet' -import safeModule from '@web3-onboard/gnosis' -import trezorModule from '@web3-onboard/trezor' -import magicModule from '@web3-onboard/magic' -import dcentModule from '@web3-onboard/dcent'; +"use client"; -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 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"; +import coinbaseModule from "@web3-onboard/coinbase"; +import infinityWalletModule from "@web3-onboard/infinity-wallet"; +import safeModule from "@web3-onboard/gnosis"; +import trezorModule from "@web3-onboard/trezor"; +import magicModule from "@web3-onboard/magic"; +import dcentModule from "@web3-onboard/dcent"; +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"; +import { useSettings } from "@/components/context/SettingsContext"; +import CryptoJS from "crypto-js"; // Still imported but not used in encryption here +import bcrypt from "bcryptjs"; // Import bcryptjs + +// Secret key (should ideally be stored in environment variables) +const SECRET_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || "my-secret-key-1234567890"; + +// Function to hash a password +const hashPassword = (password: string): string => { + const salt = bcrypt.genSaltSync(10); + return bcrypt.hashSync(password, salt); +}; + +// Function to hash data (replacing encryptData) +const encryptData = (data: string): string => { + return bcrypt.hashSync(data, 10); +}; -const INFURA_KEY = '7d389678fba04ceb9510b2be4fff5129'; // Replace with your Infura key +// Function to "decrypt" data (not possible with bcrypt) +const decryptData = (encryptedData: string): string => { + // bcrypt is a one-way hashing algorithm, so we cannot decrypt + throw new Error("Cannot decrypt data hashed with bcrypt"); +}; + +// Rest of your wallet configurations remain unchanged +const dcent = dcentModule(); +const INFURA_KEY = "7d389678fba04ceb9510b2be4fff5129"; -// Initialize WalletConnect with projectId const walletConnect = walletConnectModule({ - projectId: 'b773e42585868b9b143bb0f1664670f1', // Replace with your WalletConnect project ID - optionalChains: [1, 137] // Optional: specify chains you want to support + projectId: "b773e42585868b9b143bb0f1664670f1", + optionalChains: [1, 137], }); - const injected = injectedModule(); const coinbase = coinbaseModule(); -const infinityWallet = infinityWalletModule() -const safe = safeModule() -const sequence = sequenceModule() -const taho = tahoModule() // Previously named Tally Ho wallet -const trust = trustModule() -const okx = okxModule() -const frontier = frontierModule() -const trezorOptions = { - email: 'test@test.com', - appUrl: 'https://www.blocknative.com' -} - -const trezor = trezorModule(trezorOptions) +const infinityWallet = infinityWalletModule(); +const safe = safeModule(); +const sequence = sequenceModule(); +const taho = tahoModule(); +const trust = trustModule(); +const okx = okxModule(); +const frontier = frontierModule(); +const trezor = trezorModule({ email: "test@test.com", appUrl: "https://www.blocknative.com" }); +const magic = magicModule({ apiKey: "pk_live_E9B0C0916678868E" }); -const magic = magicModule({ - apiKey: 'pk_live_E9B0C0916678868E' -}) - -const wallets = [infinityWallet, +const wallets = [ + infinityWallet, sequence, injected, trust, @@ -60,191 +78,187 @@ const wallets = [infinityWallet, dcent, walletConnect, safe, - magic]; + magic, +]; const chains = [ - { - id: '0x1', - token: 'ETH', - label: 'Ethereum Mainnet', - rpcUrl: `https://mainnet.infura.io/v3/${INFURA_KEY}`, - }, - { - id: 11155111, - token: 'ETH', - label: 'Sepolia', - rpcUrl: 'https://rpc.sepolia.org/' - }, - { - id: '0x13881', - token: 'MATIC', - label: 'Polygon - Mumbai', - rpcUrl: 'https://matic-mumbai.chainstacklabs.com', - }, - { - id: '0x38', - token: 'BNB', - label: 'Binance', - rpcUrl: 'https://bsc-dataseed.binance.org/' - }, - { - id: '0xA', - token: 'OETH', - label: 'OP Mainnet', - rpcUrl: 'https://mainnet.optimism.io' - }, - { - id: '0xA4B1', - token: 'ARB-ETH', - label: 'Arbitrum', - rpcUrl: 'https://rpc.ankr.com/arbitrum' - }, - { - id: '0xa4ec', - token: 'ETH', - label: 'Celo', - rpcUrl: 'https://1rpc.io/celo' - }, - { - id: 666666666, - token: 'DEGEN', - label: 'Degen', - rpcUrl: 'https://rpc.degen.tips' - }, - { - id: 2192, - token: 'SNAX', - label: 'SNAX Chain', - rpcUrl: 'https://mainnet.snaxchain.io' - } + { id: "0x1", token: "ETH", label: "Ethereum Mainnet", rpcUrl: `https://mainnet.infura.io/v3/${INFURA_KEY}` }, + { id: "11155111", token: "ETH", label: "Sepolia", rpcUrl: "https://rpc.sepolia.org/" }, + { id: "0x13881", token: "MATIC", label: "Polygon - Mumbai", rpcUrl: "https://matic-mumbai.chainstacklabs.com" }, + { id: "0x38", token: "BNB", label: "Binance", rpcUrl: "https://bsc-dataseed.binance.org/" }, + { id: "0xA", token: "OETH", label: "OP Mainnet", rpcUrl: "https://mainnet.optimism.io" }, + { id: "0xA4B1", token: "ARB-ETH", label: "Arbitrum", rpcUrl: "https://rpc.ankr.com/arbitrum" }, + { id: "0xa4ec", token: "ETH", label: "Celo", rpcUrl: "https://1rpc.io/celo" }, + { id: "666666666", token: "DEGEN", label: "Degen", rpcUrl: "https://rpc.degen.tips" }, + { id: "2192", token: "SNAX", label: "SNAX Chain", rpcUrl: "https://mainnet.snaxchain.io" }, ]; const appMetadata = { - name: 'CryptoPath', - //icon: '', // Replace with your actual icon - description: 'Login to CryptoPath with your wallet', + name: "CryptoPath", + description: "Login to CryptoPath with your wallet", recommendedInjectedWallets: [ - { name: 'MetaMask', url: 'https://metamask.io' }, - { name: 'Coinbase', url: 'https://wallet.coinbase.com/' }, + { name: "MetaMask", url: "https://metamask.io" }, + { name: "Coinbase", url: "https://wallet.coinbase.com/" }, ], }; -const web3Onboard = init({ - wallets, - chains, - appMetadata, -}); +const web3Onboard = init({ wallets, chains, appMetadata }); function LoginPageContent() { const router = useRouter(); + const { signInWithWalletConnect, signIn } = useAuth(); + const { updateProfile, addWallet, syncWithSupabase } = useSettings(); - // Form state - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [emailError, setEmailError] = useState(''); - const [passwordError, setPasswordError] = useState(''); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); const [showPassword, setShowPassword] = useState(false); - const [isLoggedOut, setIsLoggedOut] = useState(false); - // Wallet state + const [isLoading, setIsLoading] = useState(false); + 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 + const checkExistingSession = async () => { + const { data: { session } } = await supabase.auth.getSession(); + if (session) { + router.push("/"); + } + }; + checkExistingSession(); + }, [router]); + + useEffect(() => { + 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 - }; - localStorage.setItem('currentUser', JSON.stringify(userData)); - window.location.href = '/'; - } - }, [wallet, router, isLoggedOut]); + setAccount({ address, ens: ens?.name || null }); - // Helper functions (using localStorage for demo purposes) - const validateEmail = (email: string) => { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email.toLowerCase()); - }; + const authenticateWithWallet = async () => { + try { + setIsLoading(true); + const { data, error } = await signInWithWalletConnect(address); + if (error) { + console.error("Wallet auth error:", error); + toast.error(`Failed to authenticate with wallet: ${error.message}`); + return; + } - const getUsers = () => { - if (typeof window !== 'undefined') { - const usersJSON = localStorage.getItem('users'); - return usersJSON ? JSON.parse(usersJSON) : []; - } - return []; - }; + updateProfile({ + username: ens?.name || formatWalletAddress(address), + profileImage: null, + backgroundImage: null, + }); + addWallet(address); + await syncWithSupabase(); - const isEmailExists = (email: string) => { - const users = getUsers(); - return users.some((user: { email: string }) => user.email === email); - }; + const publicUserData = { + walletAddress: address, + name: ens?.name || formatWalletAddress(address), + isLoggedIn: true, + }; + // Hash before storing in localStorage + localStorage.setItem("userDisplayInfo", encryptData(JSON.stringify(publicUserData))); + localStorage.setItem("userToken", encryptData(data.session?.access_token || "")); - 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; - }; + 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); + } + }; - const handleSubmit = (e: React.FormEvent) => { + authenticateWithWallet(); + } + }, [wallet, router, isLoggedOut, signInWithWalletConnect, updateProfile, addWallet, syncWithSupabase]); + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setEmailError(''); - setPasswordError(''); + setEmailError(""); + setPasswordError(""); + setIsLoading(true); - let valid = true; + try { + // Hash the password before sending it to the signIn function + const hashedPassword = hashPassword(password); + const { data, error } = await signIn(email, hashedPassword); // Assuming signIn accepts hashed password + if (error) { + if (error.message.includes("email")) setEmailError(error.message); + else if (error.message.includes("password")) setPasswordError(error.message); + else toast.error(error.message); + setIsLoading(false); + return; + } - if (!validateEmail(email)) { - setEmailError('Please enter a valid email address.'); - valid = false; - } else if (!isEmailExists(email)) { - setEmailError('Email does not exist.'); - valid = false; - } + if (!data.user) { + toast.error("Something went wrong with the login"); + setIsLoading(false); + return; + } - if (valid && !validatePasswordForUser(email, password)) { - setPasswordError('Incorrect password.'); - valid = false; - } + const { data: profileData } = await supabase + .from("profiles") + .select("*") + .eq("id", data.user.id) + .single(); + + updateProfile({ + username: profileData?.display_name || email.split("@")[0], + profileImage: profileData?.profile_image || null, + backgroundImage: profileData?.background_image || null, + }); + await syncWithSupabase(); - 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(); + const publicUserData = { + id: data.user.id, + name: profileData?.display_name || email.split("@")[0], + email, + isLoggedIn: true, + }; + // Hash before storing in localStorage + localStorage.setItem("currentUser", encryptData(JSON.stringify(publicUserData))); + localStorage.setItem("userToken", encryptData(data.session?.access_token || "")); + + toast.success("Login successful!"); + router.push("/"); + } catch (error: any) { + 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(); } else { - disconnect({ label: wallet.label }); // Ngắt kết nối ví + disconnect({ label: wallet.label }); setAccount(null); - setIsLoggedOut(true); // Đánh dấu đã logout - localStorage.removeItem('currentUser'); - router.push('/login'); // Chuyển hướng về trang login + setIsLoggedOut(true); + await supabase.auth.signOut(); + localStorage.removeItem("userDisplayInfo"); + localStorage.removeItem("userToken"); + router.push("/login"); } }; + // The rest of your JSX remains unchanged return ( <>
@@ -258,14 +272,12 @@ function LoginPageContent() {

Welcome back

- Login to your{' '} + Login to your{" "} CryptoPath account

- + setEmail(e.target.value)} + disabled={isLoading} /> {emailError && {emailError}}
- +
setPassword(e.target.value)} + disabled={isLoading} />
- {passwordError && ( - {passwordError} - )} + {passwordError && {passwordError}}
Or continue with
-
- {/* Social login buttons (icons only) */} - +
@@ -398,44 +364,28 @@ function LoginPageContent() { id="connectButton" type="button" onClick={handleWalletConnect} - disabled={connecting} + disabled={connecting || isLoading} className="flex items-center justify-center w-full border border-white rounded-md py-2 px-4 hover:bg-gray-800" > - - + + Login with Wallet
- Don't have an account?{' '} - - Sign up - + Don't have an account?{" "} + Sign up
- By clicking continue, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - . + By clicking continue, you agree to our{" "} + Terms of Service{" "} + and{" "} + Privacy Policy.
diff --git a/app/page.tsx b/app/page.tsx index d8c6a5e..ef3d4f3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,10 @@ 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/home/EthPriceLine'; +import CryptoPathExplorer from '@/components/home/CryptoExplorer'; +import TrendingProjects from '@/components/home/TrendingProjects'; +import TrendingNFTCollections from '@/components/home/TrendingNFTs'; import FAQ from './FAQ'; import AOS from 'aos'; import 'aos/dist/aos.css'; @@ -18,94 +22,94 @@ type Language = 'en' | 'vi'; // Translation object const translations = { en: { - vietnamPremierCrypto: "Vietnam's Premier Crypto Platform", - joinAllInOne: "Join the all-in-one crypto ", - appInVietnam: "app in Vietnam", + vietnamPremierCrypto: "Vietnam's Premier Blockchain Explorer", + joinAllInOne: "Your all-in-one crypto ", + appInVietnam: "transaction explorer", emailPlaceholder: "Your Email Address...", signUpSuccess: "Sign Up Successfully!", processing: "Processing...", tryCryptoPath: "Try CryptoPath", - tradeLikePro: "Trade like ", - aPro: "a pro", - getLowestFees: "Get the lowest fees, fastest transactions, powerful APIs, and more", - oneApplication: "One Application. ", - infinitePotential: "Infinite Potential", - exploreNFTMarketplace: "Explore the world's best NFT marketplace, DEX, and wallets supporting all your favorite chains.", - exploreDecentralized: "Explore decentralized applications and experience cutting-edge blockchain technology.", - exchange: "Exchange", - web3: "Web3", - accompanyingYou: "Accompanying You ", - everyStep: "Every Step of the Way", - fromCryptoTransactions: "From cryptocurrency transactions to your first NFT purchase, CryptoPath will guide you through the entire process.", - believeInYourself: "Believe in yourself and never stop learning.", + tradeLikePro: "Track transactions ", + aPro: "like never before", + getLowestFees: "Real-time transaction monitoring, comprehensive analytics, and powerful visualization tools", + oneApplication: "One Platform. ", + infinitePotential: "Complete Insights", + exploreNFTMarketplace: "Track real-time cryptocurrency transactions, monitor market trends, and analyze blockchain metrics with our comprehensive dashboard.", + exploreDecentralized: "Explore detailed transaction histories, wallet analytics, and network statistics with our powerful blockchain explorer.", + exchange: "Analytics", + web3: "Explorer", + accompanyingYou: "Your Gateway to ", + everyStep: "Blockchain Data", + fromCryptoTransactions: "From real-time transaction tracking to comprehensive market analysis, CryptoPath provides you with all the tools you need to understand blockchain activity.", + believeInYourself: "Make informed decisions with data-driven insights.", meetTheTeam: "Meet the ", team: "Team", - willingToListen: "We are always willing to listen to everyone!", - whatIsCryptoPath: "What is ", + willingToListen: "Dedicated to building the best blockchain explorer!", + whatIsCryptoPath: "Why ", cryptoPath: "CryptoPath?", - hearFromTopIndustry: "Hear from top industry leaders to understand", - whyCryptoPathIsFavorite: "why CryptoPath is everyone's favorite application.", + hearFromTopIndustry: "A powerful blockchain explorer that helps you", + whyCryptoPathIsFavorite: "track, analyze, and understand cryptocurrency transactions.", learnMore: "Learn More", - whatIsCryptocurrency: "What is Cryptocurrency?", - explainingNewCurrency: "Explaining the \"new currency of the world\"", - redefiningSystem: "Redefining the system", - welcomeToWeb3: "Welcome to Web3", - whatIsBlockchain: "What is Blockchain?", - understandBlockchain: "Understand how Blockchain works", + whatIsCryptocurrency: "Real-Time Analytics", + explainingNewCurrency: "Track market trends and transaction flows", + redefiningSystem: "Transaction Explorer", + welcomeToWeb3: "Monitor blockchain activity in real-time", + whatIsBlockchain: "Network Statistics", + understandBlockchain: "Comprehensive blockchain metrics and insights", trustedBy: "Trusted", - industryLeaders: "by industry leaders", - testimonialText: "\"CryptoPath is an amazing platform for tracking transactions. I can't even picture what the world would be like without it\"", + industryLeaders: "by crypto enthusiasts", + testimonialText: "\"CryptoPath provides the most comprehensive and user-friendly blockchain explorer I've ever used. The real-time analytics and transaction tracking are invaluable.\"", founderOf: "Founder of CryptoPath", - readyToStart: "Ready to start your crypto journey?", - joinThousands: "Join thousands of Vietnamese users who are already trading, investing, and earning with CryptoPath.", - downloadNow: "Download Now", + readyToStart: "Ready to explore the blockchain?", + joinThousands: "Join thousands of users who are already using CryptoPath to track and analyze cryptocurrency transactions.", + downloadNow: "Start Exploring", pleaseEnterEmail: "Please enter your email address", pleaseEnterValidEmail: "Please enter a valid email address", errorOccurred: "An error occurred while registering!", registrationSuccessful: "Registration successful! Please check your email." }, vi: { - vietnamPremierCrypto: "Nền tảng Crypto hàng đầu Việt Nam", - joinAllInOne: "Tham gia ứng dụng crypto ", - appInVietnam: "tất cả trong một ở Việt Nam", + vietnamPremierCrypto: "Nền Tảng Khám Phá Blockchain Hàng Đầu Việt Nam", + joinAllInOne: "Nền tảng theo dõi giao dịch ", + appInVietnam: "tiền điện tử toàn diện", emailPlaceholder: "Địa chỉ Email của bạn...", signUpSuccess: "Đăng ký thành công!", processing: "Đang xử lý...", - tryCryptoPath: "Dùng thử CryptoPath", - tradeLikePro: "Giao dịch như ", - aPro: "chuyên gia", - getLowestFees: "Nhận phí thấp nhất, giao dịch nhanh nhất, API mạnh mẽ và nhiều hơn nữa", - oneApplication: "Một ứng dụng. ", - infinitePotential: "Tiềm năng vô hạn", - exploreNFTMarketplace: "Khám phá thị trường NFT, DEX tốt nhất thế giới và ví hỗ trợ tất cả các chuỗi yêu thích của bạn.", - exploreDecentralized: "Khám phá các ứng dụng phi tập trung và trải nghiệm công nghệ blockchain tiên tiến.", - exchange: "Sàn giao dịch", - web3: "Web3", - accompanyingYou: "Đồng hành cùng bạn ", - everyStep: "trong từng bước đi", - fromCryptoTransactions: "Từ giao dịch tiền điện tử đến việc mua NFT đầu tiên, CryptoPath sẽ hướng dẫn bạn qua toàn bộ quá trình.", - believeInYourself: "Hãy tin vào chính mình và không ngừng học hỏi.", + tryCryptoPath: "Thử CryptoPath", + tradeLikePro: "Theo dõi giao dịch ", + aPro: "theo cách mới", + getLowestFees: "Giám sát giao dịch thời gian thực, phân tích toàn diện và công cụ trực quan mạnh mẽ", + oneApplication: "Một nền tảng. ", + infinitePotential: "Thông tin đầy đủ", + exploreNFTMarketplace: "Theo dõi giao dịch tiền điện tử thời gian thực, giám sát xu hướng thị trường và phân tích các chỉ số blockchain với bảng điều khiển toàn diện của chúng tôi.", + exploreDecentralized: "Khám phá lịch sử giao dịch chi tiết, phân tích ví và thống kê mạng lưới với công cụ khám phá blockchain mạnh mẽ của chúng tôi.", + exchange: "Phân tích", + web3: "Khám phá", + accompanyingYou: "Cổng thông tin ", + everyStep: "Blockchain của bạn", + fromCryptoTransactions: "Từ theo dõi giao dịch thời gian thực đến phân tích thị trường toàn diện, CryptoPath cung cấp cho bạn tất cả các công cụ cần thiết để hiểu hoạt động blockchain.", + believeInYourself: "Đưa ra quyết định dựa trên dữ liệu thực tế.", meetTheTeam: "Gặp gỡ ", team: "Đội ngũ", - willingToListen: "Chúng tôi luôn sẵn sàng lắng nghe mọi người!", - whatIsCryptoPath: "CryptoPath ", - cryptoPath: "là gì?", - hearFromTopIndustry: "Lắng nghe từ các nhà lãnh đạo hàng đầu trong ngành để hiểu", - whyCryptoPathIsFavorite: "tại sao CryptoPath là ứng dụng yêu thích của mọi người.", + willingToListen: "Luôn nỗ lực xây dựng nền tảng khám phá blockchain tốt nhất!", + whatIsCryptoPath: "Tại sao chọn ", + cryptoPath: "CryptoPath?", + hearFromTopIndustry: "Một công cụ khám phá blockchain mạnh mẽ giúp bạn", + whyCryptoPathIsFavorite: "theo dõi, phân tích và hiểu các giao dịch tiền điện tử.", learnMore: "Tìm hiểu thêm", - whatIsCryptocurrency: "Tiền điện tử là gì?", - explainingNewCurrency: "Giải thích về \"đồng tiền mới của thế giới\"", - redefiningSystem: "Định nghĩa lại hệ thống", - welcomeToWeb3: "Chào mừng đến với Web3", - whatIsBlockchain: "Blockchain là gì?", - understandBlockchain: "Hiểu cách Blockchain hoạt động", + whatIsCryptocurrency: "Phân tích thời gian thực", + explainingNewCurrency: "Theo dõi xu hướng thị trường và luồng giao dịch", + redefiningSystem: "Khám phá giao dịch", + welcomeToWeb3: "Giám sát hoạt động blockchain theo thời gian thực", + whatIsBlockchain: "Thống kê mạng lưới", + understandBlockchain: "Số liệu và thông tin blockchain toàn diện", trustedBy: "Được tin dùng", - industryLeaders: "bởi các nhà lãnh đạo ngành", - testimonialText: "\"CryptoPath là một nền tảng tuyệt vời để theo dõi giao dịch. Tôi thậm chí không thể tưởng tượng thế giới sẽ như thế nào nếu không có nó\"", + industryLeaders: "bởi cộng đồng crypto", + testimonialText: "\"CryptoPath cung cấp công cụ khám phá blockchain toàn diện và thân thiện nhất mà tôi từng sử dụng. Phân tích thời gian thực và theo dõi giao dịch là vô giá.\"", founderOf: "Nhà sáng lập CryptoPath", - readyToStart: "Sẵn sàng bắt đầu hành trình tiền điện tử của bạn?", - joinThousands: "Tham gia cùng hàng nghìn người dùng Việt Nam đang giao dịch, đầu tư và kiếm tiền với CryptoPath.", - downloadNow: "Tải xuống ngay", + readyToStart: "Sẵn sàng khám phá blockchain?", + joinThousands: "Tham gia cùng hàng nghìn người dùng đang sử dụng CryptoPath để theo dõi và phân tích giao dịch tiền điện tử.", + downloadNow: "Bắt đầu khám phá", pleaseEnterEmail: "Vui lòng nhập địa chỉ email của bạn", pleaseEnterValidEmail: "Vui lòng nhập địa chỉ email hợp lệ", errorOccurred: "Đã xảy ra lỗi khi đăng ký!", @@ -236,7 +240,8 @@ const HomePage = () => { return (
- + +
{/* Description Section */}
@@ -257,7 +262,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`} /> @@ -287,7 +292,7 @@ const HomePage = () => {
- + {/* Trade Like a Pro Section */}
@@ -297,9 +302,9 @@ const HomePage = () => {

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

- + {/* Evolution Illustration Section */}

{t.accompanyingYou}{t.everyStep}

@@ -362,8 +367,8 @@ const HomePage = () => {

-
-