ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 6a2bfa021b6221894d7d7b2b5b193aea7c106ca6 172 lines 6.8 kB view raw
1import { useState, useEffect, useRef } from "react"; 2import { createPortal } from "react-dom"; 3import { Heart, Home, LogOut, ChevronDown } from "lucide-react"; 4import ThemeControls from "./ThemeControls"; 5import FireflyLogo from "../assets/at-firefly-logo.svg?react"; 6import AvatarWithFallback from "./common/AvatarWithFallback"; 7 8interface atprotoSession { 9 did: string; 10 handle: string; 11 displayName?: string; 12 avatar?: string; 13 description?: string; 14} 15 16interface AppHeaderProps { 17 session: atprotoSession | null; 18 onLogout: () => void; 19 onNavigate: (step: "home" | "login") => void; 20 currentStep: string; 21 isDark?: boolean; 22 reducedMotion?: boolean; 23 onToggleTheme?: () => void; 24 onToggleMotion?: () => void; 25} 26 27export default function AppHeader({ 28 session, 29 onLogout, 30 onNavigate, 31 currentStep, 32 isDark = false, 33 reducedMotion = false, 34 onToggleTheme, 35 onToggleMotion, 36}: AppHeaderProps) { 37 const [showMenu, setShowMenu] = useState(false); 38 const [menuPosition, setMenuPosition] = useState({ top: 0, right: 0 }); 39 const menuRef = useRef<HTMLDivElement>(null); 40 const buttonRef = useRef<HTMLButtonElement>(null); 41 42 useEffect(() => { 43 function handleClickOutside(event: MouseEvent) { 44 if ( 45 menuRef.current && 46 !menuRef.current.contains(event.target as Node) && 47 buttonRef.current && 48 !buttonRef.current.contains(event.target as Node) 49 ) { 50 setShowMenu(false); 51 } 52 } 53 document.addEventListener("mousedown", handleClickOutside); 54 return () => document.removeEventListener("mousedown", handleClickOutside); 55 }, []); 56 57 useEffect(() => { 58 if (showMenu && buttonRef.current) { 59 const rect = buttonRef.current.getBoundingClientRect(); 60 setMenuPosition({ 61 top: rect.bottom + 8, 62 right: window.innerWidth - rect.right, 63 }); 64 } 65 }, [showMenu]); 66 67 return ( 68 <div className="bg-white dark:bg-slate-900 border-b-2 border-cyan-500/30 dark:border-purple-500/30 backdrop-blur-xl relative z-50"> 69 <div className="max-w-6xl mx-auto px-4 py-1"> 70 <div className="flex items-center justify-between"> 71 <button 72 onClick={() => onNavigate(session ? "home" : "login")} 73 className="flex items-center space-x-3 hover:opacity-80 transition-opacity focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400 rounded-lg px-2 py-1" 74 > 75 <FireflyLogo className="w-14 h-10" /> 76 <h1 className="font-display text-2xl font-bold text-purple-950 dark:text-cyan-50"> 77 ATlast 78 </h1> 79 </button> 80 81 <div className="flex items-center space-x-4"> 82 {onToggleTheme && onToggleMotion && ( 83 <ThemeControls 84 isDark={isDark} 85 reducedMotion={reducedMotion} 86 onToggleTheme={onToggleTheme} 87 onToggleMotion={onToggleMotion} 88 /> 89 )} 90 {session && ( 91 <> 92 <button 93 ref={buttonRef} 94 onClick={() => setShowMenu(!showMenu)} 95 className="flex items-center space-x-3 px-3 py-1 rounded-lg hover:bg-purple-50 dark:hover:bg-slate-800 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-amber-400" 96 > 97 <AvatarWithFallback 98 avatar={session?.avatar} 99 handle={session?.handle || ""} 100 size="sm" 101 /> 102 <span className="text-sm font-medium text-purple-950 dark:text-cyan-50 hidden sm:inline"> 103 @{session?.handle} 104 </span> 105 <ChevronDown 106 className={`w-4 h-4 text-purple-750 dark:text-cyan-250 transition-transform ${showMenu ? "rotate-180" : ""}`} 107 /> 108 </button> 109 110 {showMenu && 111 createPortal( 112 <div 113 ref={menuRef} 114 className="fixed w-64 bg-white dark:bg-slate-900 rounded-lg shadow-2xl border-2 border-cyan-500/30 dark:border-purple-500/30 py-2 z-[9999]" 115 style={{ 116 top: `${menuPosition.top}px`, 117 right: `${menuPosition.right}px`, 118 }} 119 > 120 <div className="px-4 py-3"> 121 <div className="font-semibold text-purple-950 dark:text-cyan-50"> 122 {session?.displayName || session.handle} 123 </div> 124 <div className="text-sm text-purple-750 dark:text-cyan-250"> 125 @{session?.handle} 126 </div> 127 </div> 128 <button 129 onClick={() => { 130 setShowMenu(false); 131 onNavigate("home"); 132 }} 133 className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-purple-50 dark:hover:bg-slate-800 transition-colors text-left" 134 > 135 <Home className="w-4 h-4 text-purple-950 dark:text-cyan-50" /> 136 <span className="text-purple-950 dark:text-cyan-50"> 137 Dashboard 138 </span> 139 </button> 140 <button 141 onClick={() => { 142 setShowMenu(false); 143 onNavigate("login"); 144 }} 145 className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-purple-50 dark:hover:bg-slate-800 transition-colors text-left" 146 > 147 <Heart className="w-4 h-4 text-purple-950 dark:text-cyan-50" /> 148 <span className="text-purple-950 dark:text-cyan-50"> 149 Login screen 150 </span> 151 </button> 152 <button 153 onClick={() => { 154 setShowMenu(false); 155 onLogout(); 156 }} 157 className="w-full flex items-center space-x-3 px-4 py-2 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-left text-red-600 dark:text-red-400" 158 > 159 <LogOut className="w-4 h-4" /> 160 <span>Log out</span> 161 </button> 162 </div>, 163 document.body, 164 )} 165 </> 166 )} 167 </div> 168 </div> 169 </div> 170 </div> 171 ); 172}