import { useState, useEffect } from 'react'; import { SearchBar } from './components/SearchBar'; import { FileList } from './components/FileList'; import { MetadataEditor } from './components/MetadataEditor'; import { BulkMetadataEditor } from './components/BulkMetadataEditor'; import { MetadataAPI } from './api/metadataApi'; import { SearchResult } from './types'; import './App.css'; function App() { const [searchResults, setSearchResults] = useState([]); const [selectedHashId, setSelectedHashId] = useState(); const [selectedHashIds, setSelectedHashIds] = useState>(new Set()); const [bulkEditMode, setBulkEditMode] = useState(false); const [isSearching, setIsSearching] = useState(false); const [isLoadingDirectHash, setIsLoadingDirectHash] = useState(false); const [error, setError] = useState(null); // Extract hash from URL path (supports /editor/file/hash or /editor/?hash=...) const getHashFromUrl = (): string | null => { const path = window.location.pathname; const fileMatch = path.match(/\/file\/([a-z0-9]+)$/i); if (fileMatch) { return fileMatch[1]; } // Fallback to query param for backwards compatibility const params = new URLSearchParams(window.location.search); return params.get('hash'); }; // Update URL when file is selected const updateUrlForFile = (hashId: string | undefined) => { const basePath = window.location.pathname.replace(/\/file\/[a-z0-9]+$/i, '').replace(/\/$/, ''); if (hashId) { const newPath = `${basePath}/file/${hashId}`; window.history.pushState({ hashId }, '', newPath); } else { window.history.pushState({}, '', basePath || '/editor/'); } }; // Check for hash parameter in URL on component mount useEffect(() => { const loadHashFromUrl = async () => { const hashParam = getHashFromUrl(); if (hashParam) { try { setIsLoadingDirectHash(true); setError(null); // Try to get metadata for this hash const metadata = await MetadataAPI.getMetadata(hashParam); // Create a search result from the metadata const searchResult: SearchResult = { hashId: hashParam, metadata: metadata, score: 1.0, }; // Set the search results and select this file setSearchResults([searchResult]); setSelectedHashId(hashParam); } catch (err: any) { setError(`Failed to load file with hash ${hashParam}: ${err.message}`); setSearchResults([]); setSelectedHashId(undefined); } finally { setIsLoadingDirectHash(false); } } }; loadHashFromUrl(); // Handle browser back/forward const handlePopState = (event: PopStateEvent) => { const hashId = event.state?.hashId || getHashFromUrl(); if (hashId) { setSelectedHashId(hashId); } else { setSelectedHashId(undefined); } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); // Empty dependency array means this runs once on mount const handleSearch = async (query: string) => { try { setIsSearching(true); setError(null); // Unified search - searches across all fields const response = await MetadataAPI.search({ query }); setSearchResults(response.results); // Auto-select first result if available if (response.results.length > 0) { setSelectedHashId(response.results[0].hashId); } else { setSelectedHashId(undefined); } } catch (err: any) { setError(err.message); setSearchResults([]); setSelectedHashId(undefined); } finally { setIsSearching(false); } }; const handleSelectFile = (hashId: string) => { if (!bulkEditMode) { setSelectedHashId(hashId); setSelectedHashIds(new Set()); updateUrlForFile(hashId); } }; const handleToggleSelection = (hashId: string) => { const newSelection = new Set(selectedHashIds); if (newSelection.has(hashId)) { newSelection.delete(hashId); } else { newSelection.add(hashId); } setSelectedHashIds(newSelection); }; const handleSelectAll = () => { const allHashIds = new Set(searchResults.map(r => r.hashId)); setSelectedHashIds(allHashIds); }; const handleClearSelection = () => { setSelectedHashIds(new Set()); setBulkEditMode(false); }; const handleBulkEditMode = (enabled: boolean) => { setBulkEditMode(enabled); if (enabled) { setSelectedHashId(undefined); } else { setSelectedHashIds(new Set()); } }; const handleMetadataSaved = () => { // Optionally refresh the search results to reflect changes console.log('Metadata saved successfully'); }; const handleBulkMetadataSaved = () => { console.log('Bulk metadata saved successfully'); handleClearSelection(); }; return (

MetaMesh Metadata Editor

Search and edit metadata for your media files

{error &&
{error}
} {isLoadingDirectHash && (
Loading file metadata...
)} {!isSearching && !isLoadingDirectHash && searchResults.length === 0 && !error && (

Welcome to MetaMesh Metadata Editor

Search for files by title, filename, file path, or hash ID to get started.

Unified search across all fields | Single file editing | Bulk editing | Direct access via URL

)} {searchResults.length > 0 && (
{bulkEditMode && selectedHashIds.size > 0 ? ( ) : selectedHashId ? ( ) : (
{bulkEditMode ? 'Select files from the list to bulk edit metadata' : 'Select a file from the list to edit its metadata'}
)}
)}
); } export default App;