import React, { useState, useEffect, useCallback, Suspense, lazy } from "react"; import { ArrowRight } from "lucide-react"; import { useAuth } from "./hooks/useAuth"; import { useSearch } from "./hooks/useSearch"; import { useFollow } from "./hooks/useFollows"; import { useFileUpload } from "./hooks/useFileUpload"; import { useTheme } from "./hooks/useTheme"; import { useNotifications } from "./hooks/useNotifications"; import Firefly from "./components/Firefly"; import NotificationContainer from "./components/common/NotificationContainer"; import ErrorBoundary from "./components/common/ErrorBoundary"; import AriaLiveAnnouncer from "./components/common/AriaLiveAnnouncer"; import { SearchResultSkeleton } from "./components/common/LoadingSkeleton"; import { DEFAULT_SETTINGS } from "./types/settings"; import type { UserSettings, SearchResult } from "./types"; import { apiClient } from "./lib/api/client"; import { ATPROTO_APPS } from "./config/atprotoApps"; import { useSettingsStore } from "./stores/useSettingsStore"; // Lazy load page components const LoginPage = lazy(() => import("./pages/Login")); const HomePage = lazy(() => import("./pages/Home")); const LoadingPage = lazy(() => import("./pages/Loading")); const ResultsPage = lazy(() => import("./pages/Results")); // Loading fallback component const PageLoader: React.FC = () => (

Loading...

); export default function App() { // Auth hook const { session, currentStep, statusMessage, setCurrentStep, setStatusMessage, login, logout, } = useAuth(); // Notifications hook (only for errors now) const { notifications, removeNotification, error } = useNotifications(); // Aria-live announcements for non-error feedback (invisible, screen-reader only) const [ariaAnnouncement, setAriaAnnouncement] = useState(""); // Theme hook const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme(); // Current platform state const [currentPlatform, setCurrentPlatform] = useState("tiktok"); // Track saved uploads to prevent duplicates const [savedUploads, setSavedUploads] = useState>(new Set()); // Settings state const userSettings = useSettingsStore((state) => state.settings); const handleSettingsUpdate = useSettingsStore((state) => state.updateSettings); // Search hook const { searchResults, setSearchResults, searchProgress, expandedResults, searchAllUsers, toggleMatchSelection, toggleExpandResult, selectAllMatches, deselectAllMatches, totalSelected, totalFound, } = useSearch(session); const currentDestinationAppId = userSettings.platformDestinations[ currentPlatform as keyof UserSettings["platformDestinations"] ]; // Follow hook const { isFollowing, followSelectedUsers } = useFollow( session, searchResults, setSearchResults, currentDestinationAppId, ); // Save results handler (proper state management) const saveResults = useCallback( async (uploadId: string, platform: string, results: SearchResult[]) => { if (!userSettings.saveData) { console.log("Data storage disabled - skipping save to database"); return; } if (savedUploads.has(uploadId)) { console.log("Upload already saved:", uploadId); return; } try { setSavedUploads((prev) => new Set(prev).add(uploadId)); await apiClient.saveResults(uploadId, platform, results); console.log("Results saved successfully:", uploadId); } catch (err) { console.error("Background save failed:", err); setSavedUploads((prev) => { const next = new Set(prev); next.delete(uploadId); return next; }); } }, [userSettings.saveData, savedUploads], ); // File upload handler const { handleFileUpload: processFileUpload } = useFileUpload( (initialResults, platform) => { setCurrentPlatform(platform); setSearchResults(initialResults); setCurrentStep("loading"); const uploadId = crypto.randomUUID(); const followLexicon = ATPROTO_APPS[currentDestinationAppId]?.followLexicon; searchAllUsers( initialResults, setStatusMessage, () => { setCurrentStep("results"); // Save results after search completes setTimeout(() => { setSearchResults((currentResults) => { if (currentResults.length > 0) { saveResults(uploadId, platform, currentResults); } return currentResults; }); }, 1000); }, followLexicon, ); }, setStatusMessage, userSettings, ); // Load previous upload handler const handleLoadUpload = useCallback( async (uploadId: string) => { try { setStatusMessage("Loading previous upload..."); setCurrentStep("loading"); const data = await apiClient.getUploadDetails(uploadId); if (data.results.length === 0) { setSearchResults([]); setCurrentPlatform("tiktok"); setCurrentStep("home"); // No visual feedback needed - empty state will show in UI setAriaAnnouncement("No previous results found."); return; } const platform = "tiktok"; setCurrentPlatform(platform); const loadedResults: SearchResult[] = data.results.map((result) => ({ ...result, sourcePlatform: platform, isSearching: false, selectedMatches: new Set( result.atprotoMatches .filter( (match) => !match.followStatus || !Object.values(match.followStatus).some((status) => status), ) .slice(0, 1) .map((match) => match.did), ), })); setSearchResults(loadedResults); setCurrentStep("results"); // Announce to screen readers only - visual feedback is navigation to results page setAriaAnnouncement( `Loaded ${loadedResults.length} results from previous upload`, ); } catch (err) { console.error("Failed to load upload:", err); error("Failed to load previous upload. Please try again."); setCurrentStep("home"); } }, [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error], ); // Login handler const handleLogin = useCallback( async (handle: string) => { if (!handle?.trim()) { error("Please enter your handle"); return; } try { await login(handle); } catch (err) { console.error("OAuth error:", err); const errorMsg = `Error starting OAuth: ${err instanceof Error ? err.message : "Unknown error"}`; setStatusMessage(errorMsg); error(errorMsg); } }, [login, error, setStatusMessage], ); // Logout handler const handleLogout = useCallback(async () => { try { await logout(); setSearchResults([]); setCurrentPlatform("tiktok"); setSavedUploads(new Set()); // No visual feedback needed - user sees login page setAriaAnnouncement("Logged out successfully"); } catch (err) { error("Failed to logout. Please try again."); } }, [logout, setSearchResults, setAriaAnnouncement, error]); return (
{/* Notification Container - errors only */} {/* Invisible announcer for screen readers - non-error feedback */} {/* Status message for screen readers - loading/progress updates */} {/* Firefly particles - only render if motion not reduced */} {!reducedMotion && ( )} {/* Skip to main content link */} Skip to main content
}> {/* Checking Session */} {currentStep === "checking" && } {/* Login Page */} {currentStep === "login" && ( )} {/* Home/Dashboard Page */} {currentStep === "home" && ( )} {/* Loading Page */} {currentStep === "loading" && ( )} {/* Results Page */} {currentStep === "results" && ( selectAllMatches(setStatusMessage)} onDeselectAll={() => deselectAllMatches(setStatusMessage)} onFollowSelected={() => followSelectedUsers(setStatusMessage)} totalSelected={totalSelected} totalFound={totalFound} isFollowing={isFollowing} currentStep={currentStep} sourcePlatform={currentPlatform} destinationAppId={currentDestinationAppId} reducedMotion={reducedMotion} isDark={isDark} onToggleTheme={toggleTheme} onToggleMotion={toggleMotion} /> )}
); }