'use client'; import { useState, useEffect, useCallback } from 'react'; import PortalLayout from '@/app/layout-portal'; import { explorerLs, explorerCat, type IPFSEntry } from '@/lib/api/explorer'; import { listPins } from '@/lib/api/pins'; import CIDInput from './components/CIDInput'; import DirectoryListing from './components/DirectoryListing'; import FilePreview from './components/FilePreview'; import Breadcrumbs from './components/Breadcrumbs'; import PinBadge from './components/PinBadge'; import GatewayLink from './components/GatewayLink'; import SearchBar from '@/components/search'; import type { SearchResult } from '@/lib/search-index'; import { isTextFile, extractText, addToIndex, clearIndex, getIndexStats } from '@/lib/search-index'; import { Search, AlertCircle, RefreshCw, Database, Loader2 } from 'lucide-react'; import Skeleton from '@/components/Skeleton'; import ErrorBoundary from '@/components/ErrorBoundary'; import { loadRecentCids, saveRecentCid, findFilename, type ExplorerState, type BreadcrumbSegment } from './helpers'; export default function ExplorerPage() { const [cid, setCid] = useState(''); const [entries, setEntries] = useState([]); const [previewCid, setPreviewCid] = useState(null); const [previewContent, setPreviewContent] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const [path, setPath] = useState([]); const [state, setState] = useState('idle'); const [error, setError] = useState(''); const [pins, setPins] = useState([]); const [recentCids] = useState(loadRecentCids); const [indexStats, setIndexStats] = useState(getIndexStats); const [rebuilding, setRebuilding] = useState(false); // Load pins from the pins page (shared localStorage or just track in-memory) function handlePinToggle(cid: string) { setPins((prev) => (prev.includes(cid) ? prev : [...prev, cid])); } async function navigateTo(targetCid: string) { setCid(targetCid); setPreviewCid(null); setPreviewContent(null); setError(''); setState('loading'); saveRecentCid(targetCid); // Simple IPNS detection — if it contains a dot, assume it's an IPNS name if (targetCid.includes('.') && !targetCid.startsWith('Qm') && !targetCid.startsWith('baf')) { setState('resolving'); } try { const result = await explorerLs(targetCid); setEntries(result); setState('loaded'); } catch (e) { const message = e instanceof Error ? e.message : 'Failed to browse CID'; setError(message); setState('error'); setEntries([]); } } async function handleNavigate(hash: string, name: string) { setPath((prev) => [...prev, { cid: hash, name }]); await navigateTo(hash); } async function handleBreadcrumbNavigate(cid: string) { // Find the index of the clicked segment const idx = path.findIndex((s) => s.cid === cid); if (idx >= 0) { setPath(path.slice(0, idx)); } else { setPath([]); } await navigateTo(cid); } async function handlePreview(hash: string) { setPreviewCid(hash); setPreviewLoading(true); setPreviewContent(null); try { const content = await explorerCat(hash); setPreviewContent(content); } catch (err) { console.error('[ExplorerPage] Failed to fetch preview content:', err); setPreviewContent(null); } finally { setPreviewLoading(false); } } function handleClosePreview() { setPreviewCid(null); setPreviewContent(null); } function handleRetry() { if (cid) navigateTo(cid); } /* ── Search ── */ function handleSearchResult(result: SearchResult) { navigateTo(result.entry.cid); } async function rebuildSearchIndex() { setRebuilding(true); try { const pinsList = await listPins(); clearIndex(); let indexed = 0; let failed = 0; for (const pin of pinsList) { if (isTextFile(pin.name) || !pin.name) { try { const text = await explorerCat(pin.cid); const extracted = extractText(text); if (extracted.length > 0) { addToIndex({ cid: pin.cid, name: pin.name || pin.cid.slice(0, 20), type: 'file', text: extracted, size: text.length, indexedAt: Date.now(), }); indexed++; } } catch (err) { console.error('[ExplorerPage] Failed to index pin:', err); failed++; } } } setIndexStats({ entries: indexed, totalChars: 0, lastIndexed: Date.now() }); } finally { setRebuilding(false); } } const isPinned = previewCid ? pins.includes(previewCid) : false; return ( {/* Page header */}

IPFS Explorer

Browse IPFS content by CID — directories, files, and pin management

{/* Search + Index rebuild */}
{/* CID input */}
{/* Recent CIDs suggestion */} {state === 'idle' && recentCids.length > 0 && (

Recent CIDs

{recentCids.slice(0, 5).map((rc) => ( ))}
)} {/* Idle state */} {state === 'idle' && (

Enter a CID to browse IPFS content — directory listings, file previews, and pin management

)} {/* Resolving IPNS — skeleton */} {state === 'resolving' && (
{Array.from({ length: 3 }).map((_, i) => (
))}
)} {/* Error state */} {state === 'error' && (

Failed to browse content

{error}

)} {/* Loaded state — breadcrumbs + directory listing */} {(state === 'loaded' || state === 'loading') && (
{/* Breadcrumbs + Pin + Gateway row */}
{/* Directory listing */} {/* File preview */} {previewCid && ( )}
)}
); }