ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
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}