Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 46 additions & 22 deletions app/NFT/collection/[collectionId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ export default function CollectionDetailsPage() {

setLoading(true);
try {
// For BNB Chain, show a loading toast to indicate it might take some time
if (networkId === '0x38' || networkId === '0x61') {
toast({
title: 'Loading BNB Chain NFTs',
description: 'This may take a moment as we fetch data from BSCScan...',
});
}

const metadata = await fetchCollectionInfo(collectionId, networkId);
setCollection({...metadata, chain: networkId});

Expand All @@ -194,7 +202,10 @@ export default function CollectionDetailsPage() {
}));

setNfts(nftsWithChain);
setTotalPages(Math.ceil(nftData.totalCount / pageSize));

// Calculate total pages - may be different for BNB Chain
const totalPagesCount = Math.max(1, Math.ceil(nftData.totalCount / pageSize));
setTotalPages(totalPagesCount);

// Extract attributes for filtering
const attributeMap: Record<string, string[]> = {};
Expand All @@ -213,9 +224,9 @@ export default function CollectionDetailsPage() {

// Add network as a filter attribute
attributeMap['Network'] = [
chainId === '0x1' ? 'Ethereum' :
chainId === '0xaa36a7' ? 'Sepolia' :
chainId === '0x38' ? 'BNB Chain' :
networkId === '0x1' ? 'Ethereum' :
networkId === '0xaa36a7' ? 'Sepolia' :
networkId === '0x38' ? 'BNB Chain' :
'BNB Testnet'
];

Expand Down Expand Up @@ -303,7 +314,7 @@ export default function CollectionDetailsPage() {
return newFilters;
});

// Clear pagination cache
// Apply filter changes
handleFilterChange();
};

Expand Down Expand Up @@ -633,10 +644,8 @@ export default function CollectionDetailsPage() {
const styles = getAttributeStyles(traitType, value);
return (
<div key={value} className="flex items-center">
<Button
variant="outline"
size="sm"
className={`text-xs h-auto py-1.5 px-2 justify-start w-full ${styles.bgClass} border ${styles.borderClass}`}
<div
className={`flex text-xs h-auto py-1.5 px-2 justify-start w-full items-center rounded-md border ${styles.borderClass} ${styles.bgClass} cursor-pointer transition-colors hover:bg-gray-800/70`}
style={{ color: styles.textColor }}
onClick={() => handleAttributeFilter(traitType, value)}
>
Expand All @@ -647,9 +656,10 @@ export default function CollectionDetailsPage() {
accentColor: chainTheme.primary,
borderColor: styles.textColor
}}
onCheckedChange={() => handleAttributeFilter(traitType, value)}
/>
{value}
</Button>
</div>
</div>
);
})}
Expand Down Expand Up @@ -708,20 +718,27 @@ export default function CollectionDetailsPage() {
{getSortedAttributeValues(traitType, values).map((value) => {
const styles = getAttributeStyles(traitType, value);
return (
<Button
<div
key={value}
variant="outline"
size="sm"
className={`text-xs justify-start ${styles.bgClass} border ${styles.borderClass}`}
className={`flex text-xs justify-start items-center gap-2 py-1.5 px-2 rounded-md border ${styles.borderClass} ${styles.bgClass} cursor-pointer transition-colors hover:bg-gray-800/70`}
style={{ color: styles.textColor }}
onClick={() => handleAttributeFilter(traitType, value)}
>
<Checkbox
checked={isAttributeSelected(traitType, value)}
className="mr-2 h-3 w-3"
/>
<span className="truncate">{value}</span>
</Button>
<div className="flex-shrink-0">
<Checkbox
checked={isAttributeSelected(traitType, value)}
className="h-3 w-3"
id={`mobile-${traitType}-${value}`}
onCheckedChange={() => handleAttributeFilter(traitType, value)}
/>
</div>
<label
htmlFor={`mobile-${traitType}-${value}`}
className="truncate cursor-pointer"
>
{value}
</label>
</div>
);
})}
</div>
Expand Down Expand Up @@ -757,8 +774,15 @@ export default function CollectionDetailsPage() {
className="pl-10 bg-gray-800/50 border-gray-700"
value={searchQuery}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
onKeyDown={(e) => e.key === 'Enter' && handleFilterChange()}
/>
<Button
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2"
onClick={handleFilterChange}
>
Search
</Button>
</div>

<div className="flex gap-2 w-full sm:w-auto">
Expand Down Expand Up @@ -874,7 +898,7 @@ export default function CollectionDetailsPage() {
attributes={selectedAttributes}
viewMode={viewMode}
onNFTClick={handleNFTClick}
itemsPerPage={20} // Reduced to exactly 20 items per page
itemsPerPage={20} // Using exactly 20 items per page
defaultPage={currentPage}
onPageChange={(page) => {
setCurrentPage(page);
Expand Down
10 changes: 5 additions & 5 deletions app/NFT/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function NFTLayout({ children }: { children: React.ReactNode }) {

// Determine active section based on URL
const isMarketplace = pathname === '/NFT';
const isCollections = pathname.includes('/NFT/collection');
const isCollections = pathname ? pathname.includes('/NFT/collection') : false;

useEffect(() => {
setMounted(true);
Expand Down Expand Up @@ -114,13 +114,13 @@ export default function NFTLayout({ children }: { children: React.ReactNode }) {
<li>
<div className="flex items-center">
<ChevronRight className="w-4 h-4 mx-1" />
<span className={pathname === '/NFT/collection' ? 'text-white' : ''}>
Collections
</span>
<Link href="/NFT/collection" className="hover:text-white">
Collections
</Link>
</div>
</li>
)}
{pathname.includes('/NFT/collection/') && pathname !== '/NFT/collection' && (
{pathname && pathname.includes('/NFT/collection/') && pathname !== '/NFT/collection' && (
<li>
<div className="flex items-center">
<ChevronRight className="w-4 h-4 mx-1" />
Expand Down
48 changes: 48 additions & 0 deletions app/api/coingecko-proxy/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

import { NextResponse } from 'next/server';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const endpoint = searchParams.get('endpoint');

if (!endpoint) {
return NextResponse.json({ error: 'Endpoint is required' }, { status: 400 });
}

try {
const response = await fetch(`https://api.coingecko.com/api/v3/${endpoint}`, {
headers: {
'Accept': 'application/json',
// Nếu bạn có API key trả phí, thêm vào đây
// 'x-cg-api-key': 'your-api-key-here',
},
});

if (!response.ok) {
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || '60'; // Mặc định 60s nếu không có header
return NextResponse.json(
{
error: 'Rate limit exceeded. Please try again later.',
retryAfter: parseInt(retryAfter),
},
{ status: 429 } // Trả về 429 thay vì ném lỗi
);
}
throw new Error(`CoinGecko API responded with status ${response.status}`);
}

const data = await response.json();
return NextResponse.json({ data });
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{ error: 'Failed to fetch from CoinGecko' },
{ status: 500 }
);
}
}

export const config = {
runtime: 'nodejs',
};
111 changes: 111 additions & 0 deletions app/api/nfts/collection/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

// Rate limiting configuration
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute window
const MAX_REQUESTS_PER_WINDOW = 50; // 50 requests per minute

// Basic in-memory rate limiter
const rateLimiter = new Map<string, { count: number, resetAt: number }>();

export async function GET(req: NextRequest) {
// Get URL from searchParams
const url = req.nextUrl.searchParams.get('url');

if (!url) {
return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 });
}

// Apply rate limiting based on IP
const ip = req.headers.get('x-forwarded-for') || 'unknown';
const clientId = String(ip);

// Check rate limit
if (!checkRateLimit(clientId)) {
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{ status: 429 }
);
}

try {
// Validate that the URL is for the expected API
if (
!url.includes('alchemy.com/nft/') &&
!url.includes('moralis.io/api/') &&
!url.includes('etherscan.io/api') &&
!url.includes('bscscan.com/api')
) {
return NextResponse.json({ error: 'Invalid API URL' }, { status: 403 });
}

// Make the request to the NFT API service
const response = await axios.get(url, {
headers: {
'Accept': 'application/json',
},
// Handle timeouts and large responses
timeout: 10000,
maxContentLength: 10 * 1024 * 1024, // 10MB max
validateStatus: (status) => status < 500,
});

// Return the data from the API
return NextResponse.json(response.data, { status: response.status });
} catch (error: any) {
console.error('API proxy error:', error);

// Handle different error types
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
return NextResponse.json({
error: 'External API error',
details: error.response.data
}, { status: error.response.status });
} else if (error.request) {
// The request was made but no response was received
return NextResponse.json({
error: 'External API timeout',
message: 'The request timed out'
}, { status: 504 });
} else {
// Something happened in setting up the request that triggered an Error
return NextResponse.json({
error: 'Server error',
message: error.message
}, { status: 500 });
}
}
}

// Rate limiting helper function
function checkRateLimit(clientId: string): boolean {
const now = Date.now();

// Clean up expired entries
for (const [id, data] of rateLimiter.entries()) {
if (data.resetAt < now) {
rateLimiter.delete(id);
}
}

// Get or create client rate limit data
let clientData = rateLimiter.get(clientId);
if (!clientData) {
clientData = { count: 0, resetAt: now + RATE_LIMIT_WINDOW };
rateLimiter.set(clientId, clientData);
} else if (clientData.resetAt < now) {
// Reset if window expired
clientData.count = 0;
clientData.resetAt = now + RATE_LIMIT_WINDOW;
}

// Check and increment
if (clientData.count >= MAX_REQUESTS_PER_WINDOW) {
return false;
}

clientData.count++;
return true;
}
16 changes: 10 additions & 6 deletions app/search-offchain/TransactionContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import TransactionTableOffChain from "@/components/search-offchain/TransactionTa
import Portfolio from "@/components/search/Portfolio"
import { useSearchParams } from "next/navigation"
import SearchBarOffChain from "@/components/search-offchain/SearchBarOffChain"
import ChainalysisDisplay from "@/components/Chainalysis"


export default function Transactions() {
const searchParams = useSearchParams()
const address = searchParams.get("address")
const address = searchParams?.get("address") ?? null
return (
<div className="min-h-screen text-white">
<main className="container mx-auto p-4">
Expand All @@ -20,11 +21,14 @@ export default function Transactions() {
{address ? (
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<WalletInfo />
<Portfolio />
</div>
<TransactionGraphOffChain />

<WalletInfo />
<TransactionGraphOffChain />

</div>
<ChainalysisDisplay address={address}/>
<div className="mb-8">
<Portfolio />
</div>
<TransactionTableOffChain />
</>
Expand Down
6 changes: 3 additions & 3 deletions app/search/TransactionContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
export default function Transactions() {
const searchParams = useSearchParams()
const router = useRouter()
const address = searchParams.get("address")
const networkParam = searchParams.get("network") || "mainnet"
const providerParam = searchParams.get("provider") || "etherscan"
const address = searchParams?.get("address") ?? null
const networkParam = searchParams?.get("network") ?? "mainnet"
const providerParam = searchParams?.get("provider") ?? "etherscan"
const [network, setNetwork] = useState(networkParam)
const [provider, setProvider] = useState(providerParam)
const [pendingTxCount, setPendingTxCount] = useState<number | null>(null)
Expand Down
Loading