ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
1import { Sparkles } from "lucide-react"; 2import { useMemo } from "react"; 3import AppHeader from "../components/AppHeader"; 4import SearchResultCard from "../components/SearchResultCard"; 5import FaviconIcon from "../components/FaviconIcon"; 6import type { AtprotoAppId, AtprotoSession, SearchResult } from "../types"; 7import { getPlatform, getAtprotoApp } from "../lib/utils/platform"; 8import VirtualizedResultsList from "../components/VirtualizedResultsList"; 9import Button from "../components/common/Button"; 10 11interface ResultsPageProps { 12 session: AtprotoSession | null; 13 onLogout: () => void; 14 onNavigate: (step: "home" | "login") => void; 15 searchResults: SearchResult[]; 16 expandedResults: Set<number>; 17 onToggleExpand: (index: number) => void; 18 onToggleMatchSelection: (resultIndex: number, did: string) => void; 19 onSelectAll: () => void; 20 onDeselectAll: () => void; 21 onFollowSelected: () => void; 22 totalSelected: number; 23 totalFound: number; 24 isFollowing: boolean; 25 currentStep: string; 26 sourcePlatform: string; 27 destinationAppId: AtprotoAppId; 28 reducedMotion?: boolean; 29 isDark?: boolean; 30 onToggleTheme?: () => void; 31 onToggleMotion?: () => void; 32} 33 34export default function ResultsPage({ 35 session, 36 onLogout, 37 onNavigate, 38 searchResults, 39 expandedResults, 40 onToggleExpand, 41 onToggleMatchSelection, 42 onSelectAll, 43 onDeselectAll, 44 onFollowSelected, 45 totalSelected, 46 totalFound, 47 isFollowing, 48 currentStep, 49 sourcePlatform, 50 destinationAppId, 51 reducedMotion = false, 52 isDark = false, 53 onToggleTheme, 54 onToggleMotion, 55}: ResultsPageProps) { 56 const platform = getPlatform(sourcePlatform); 57 const destinationApp = getAtprotoApp(destinationAppId); 58 const PlatformIcon = platform.icon; 59 60 // Memoize sorted results to avoid re-sorting on every render 61 const sortedResults = useMemo(() => { 62 return [...searchResults].sort((a, b) => { 63 // 1. Users with matches first 64 const aHasMatches = a.atprotoMatches.length > 0 ? 0 : 1; 65 const bHasMatches = b.atprotoMatches.length > 0 ? 0 : 1; 66 if (aHasMatches !== bHasMatches) return aHasMatches - bHasMatches; 67 68 // 2. For matched users, sort by highest posts count of their top match 69 if (a.atprotoMatches.length > 0 && b.atprotoMatches.length > 0) { 70 const aTopPosts = a.atprotoMatches[0]?.postCount || 0; 71 const bTopPosts = b.atprotoMatches[0]?.postCount || 0; 72 if (aTopPosts !== bTopPosts) return bTopPosts - aTopPosts; 73 74 // 3. Then by followers count 75 const aTopFollowers = a.atprotoMatches[0]?.followerCount || 0; 76 const bTopFollowers = b.atprotoMatches[0]?.followerCount || 0; 77 if (aTopFollowers !== bTopFollowers) 78 return bTopFollowers - aTopFollowers; 79 } 80 81 // 4. Username as tiebreaker 82 return a.sourceUser.username.localeCompare(b.sourceUser.username); 83 }); 84 }, [searchResults]); 85 86 return ( 87 <div className="min-h-screen pb-24"> 88 <AppHeader 89 session={session} 90 onLogout={onLogout} 91 onNavigate={onNavigate} 92 currentStep={currentStep} 93 isDark={isDark} 94 reducedMotion={reducedMotion} 95 onToggleTheme={onToggleTheme} 96 onToggleMotion={onToggleMotion} 97 /> 98 99 {/* Platform Info Banner */} 100 <div className="bg-firefly-banner dark:bg-firefly-banner-dark text-white relative overflow-hidden"> 101 {!reducedMotion && ( 102 <div className="absolute inset-0 opacity-20" aria-hidden="true"> 103 {[...Array(10)].map((_, i) => { 104 const animations = ["animate-float-1", "animate-float-2", "animate-float-3"]; 105 return ( 106 <div 107 key={i} 108 className={`absolute w-1 h-1 bg-white rounded-full ${animations[i % 3]}`} 109 style={{ 110 left: `${Math.random() * 100}%`, 111 top: `${Math.random() * 100}%`, 112 }} 113 /> 114 ); 115 })} 116 </div> 117 )} 118 <div className="max-w-3xl mx-auto px-4 py-6 relative"> 119 <div className="flex items-center justify-between"> 120 <div className="flex items-center space-x-4"> 121 <div className="w-12 h-12 bg-white/20 backdrop-blur rounded-xl flex items-center justify-center shadow-lg"> 122 <Sparkles className="w-6 h-6 text-white" /> 123 </div> 124 <div> 125 <h2 className="text-xl font-bold"> 126 {totalFound} Connections Found! 127 </h2> 128 <p className="text-white/95 text-sm"> 129 From {searchResults.length} {platform.name} follows 130 </p> 131 </div> 132 </div> 133 {totalSelected > 0 && ( 134 <div className="text-right"> 135 <div className="text-2xl font-bold">{totalSelected}</div> 136 <div className="text-xs font-medium">selected</div> 137 </div> 138 )} 139 </div> 140 </div> 141 </div> 142 143 {/* Action Buttons */} 144 <div className="bg-white/95 dark:bg-slate-900 border-b-2 border-cyan-500/30 dark:border-purple-500/30 sticky top-0 z-10 backdrop-blur-sm"> 145 <div className="max-w-3xl mx-auto px-4 py-3 flex space-x-2"> 146 <Button onClick={onSelectAll} variant="primary" className="flex-1"> 147 Select All 148 </Button> 149 <Button 150 onClick={onDeselectAll} 151 variant="secondary" 152 className="flex-1" 153 > 154 Clear 155 </Button> 156 </div> 157 </div> 158 159 {/* Feed Results */} 160 <div className="max-w-3xl mx-auto px-4 py-4"> 161 <VirtualizedResultsList 162 results={sortedResults} 163 expandedResults={expandedResults} 164 onToggleExpand={onToggleExpand} 165 onToggleMatchSelection={onToggleMatchSelection} 166 sourcePlatform={sourcePlatform} 167 destinationAppId={destinationAppId} 168 /> 169 </div> 170 171 {/* Fixed Bottom Action Bar */} 172 {totalSelected > 0 && ( 173 <div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-transparent dark:from-slate-900 dark:via-slate-900 dark:to-transparent pt-8 pb-6"> 174 <div className="max-w-3xl mx-auto px-4"> 175 <button 176 onClick={onFollowSelected} 177 disabled={isFollowing} 178 className="w-full bg-firefly-banner dark:bg-firefly-banner-dark text-white hover:from-amber-600 hover:via-orange-600 hover:to-pink-600 py-5 rounded-2xl font-bold text-lg transition-all shadow-2xl hover:shadow-3xl flex items-center justify-center space-x-3 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100" 179 > 180 <FaviconIcon 181 url={destinationApp.icon} 182 alt={destinationApp.name} 183 className="w-5 h-5" 184 useButtonStyling={true} 185 /> 186 <span> 187 Light Up {totalSelected} Connection 188 {totalSelected === 1 ? "" : "s"} 189 </span> 190 </button> 191 </div> 192 </div> 193 )} 194 </div> 195 ); 196}