(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

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

at main 406 lines 15 kB view raw
1import { useStore } from "@nanostores/react"; 2import { 3 Bell, 4 Bookmark, 5 Folder, 6 Highlighter, 7 Home, 8 LogOut, 9 MessageSquareText, 10 PenSquare, 11 Search, 12 Settings, 13 User, 14 X, 15} from "lucide-react"; 16import { useEffect, useState } from "react"; 17import { getUnreadNotificationCount } from "../../api/client"; 18import { $user, logout } from "../../store/auth"; 19import { AppleIcon } from "../common/Icons"; 20import { useTranslation } from "react-i18next"; 21 22interface MobileNavProps { 23 currentPath?: string; 24 onNavigate?: (path: string) => void; 25} 26 27export default function MobileNav({ 28 currentPath: initialPath, 29 onNavigate, 30}: MobileNavProps) { 31 const { t } = useTranslation(); 32 const user = useStore($user); 33 const [currentPath, setCurrentPath] = useState(initialPath || "/"); 34 const [isMenuOpen, setIsMenuOpen] = useState(false); 35 const [unreadCount, setUnreadCount] = useState(0); 36 37 const isAuthenticated = !!user; 38 39 const isActive = (path: string) => { 40 if (path === "/") return currentPath === "/"; 41 return currentPath.startsWith(path); 42 }; 43 44 useEffect(() => { 45 if (isAuthenticated) { 46 getUnreadNotificationCount() 47 .then((count) => setUnreadCount(count || 0)) 48 .catch(() => {}); 49 } 50 }, [isAuthenticated]); 51 52 const closeMenu = () => setIsMenuOpen(false); 53 54 return ( 55 <> 56 {isMenuOpen && ( 57 <div 58 className="fixed inset-0 bg-black/40 z-40 md:hidden" 59 onClick={closeMenu} 60 /> 61 )} 62 63 {isMenuOpen && ( 64 <div 65 className="fixed left-0 right-0 z-50 md:hidden animate-slide-up" 66 style={{ bottom: "calc(3.5rem + env(safe-area-inset-bottom))" }} 67 > 68 <div className="mx-2 mb-2 bg-white dark:bg-surface-900 rounded-2xl shadow-xl border border-surface-200 dark:border-surface-700 overflow-hidden"> 69 <div className="flex justify-center pt-3 pb-1"> 70 <div className="w-8 h-1 bg-surface-200 dark:bg-surface-600 rounded-full" /> 71 </div> 72 73 <div className="p-2"> 74 {isAuthenticated && user ? ( 75 <> 76 <a 77 href={`/profile/${user.did}`} 78 className="flex items-center gap-3 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors" 79 onClick={(e) => { 80 if (onNavigate) { 81 e.preventDefault(); 82 onNavigate(`/profile/${user.did}`); 83 } 84 closeMenu(); 85 }} 86 > 87 {user.avatar ? ( 88 <img 89 src={user.avatar} 90 alt="" 91 className="w-9 h-9 rounded-full object-cover shrink-0" 92 /> 93 ) : ( 94 <div className="w-9 h-9 rounded-full bg-surface-100 dark:bg-surface-700 flex items-center justify-center shrink-0"> 95 <User size={16} className="text-surface-500" /> 96 </div> 97 )} 98 <div className="flex flex-col min-w-0"> 99 <span className="font-semibold text-surface-900 dark:text-white text-sm truncate"> 100 {user.displayName || user.handle} 101 </span> 102 <span className="text-xs text-surface-400 dark:text-surface-500 truncate"> 103 @{user.handle} 104 </span> 105 </div> 106 </a> 107 108 <div className="h-px bg-surface-100 dark:bg-surface-700 my-1 mx-3" /> 109 110 <div className="grid grid-cols-2 gap-1"> 111 {[ 112 { 113 href: "/annotations", 114 icon: MessageSquareText, 115 label: t("nav.annotations"), 116 }, 117 { 118 href: "/highlights", 119 icon: Highlighter, 120 label: t("nav.highlights"), 121 }, 122 { 123 href: "/bookmarks", 124 icon: Bookmark, 125 label: t("nav.bookmarks"), 126 }, 127 { 128 href: "/collections", 129 icon: Folder, 130 label: t("nav.collections"), 131 }, 132 ].map(({ href, icon: Icon, label }) => ( 133 <a 134 key={href} 135 href={href} 136 className="flex items-center gap-2.5 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors text-surface-700 dark:text-surface-200" 137 onClick={(e) => { 138 if (onNavigate) { 139 e.preventDefault(); 140 onNavigate(href); 141 } 142 closeMenu(); 143 }} 144 > 145 <Icon size={16} className="shrink-0" /> 146 <span className="text-sm font-medium truncate"> 147 {label} 148 </span> 149 </a> 150 ))} 151 </div> 152 153 <div className="h-px bg-surface-100 dark:bg-surface-700 my-1 mx-3" /> 154 155 <div className="flex gap-1"> 156 <a 157 href="/settings" 158 className="flex-1 flex items-center gap-2.5 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors text-surface-700 dark:text-surface-200" 159 onClick={(e) => { 160 if (onNavigate) { 161 e.preventDefault(); 162 onNavigate("/settings"); 163 } 164 closeMenu(); 165 }} 166 > 167 <Settings size={16} className="shrink-0" /> 168 <span className="text-sm font-medium"> 169 {t("nav.settings")} 170 </span> 171 </a> 172 173 <a 174 href="https://www.icloud.com/shortcuts/1e33ebf52f55431fae1e187cfe9738c3" 175 target="_blank" 176 rel="noopener noreferrer" 177 className="flex-1 flex items-center gap-2.5 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors text-surface-700 dark:text-surface-200" 178 onClick={closeMenu} 179 > 180 <AppleIcon size={16} /> 181 <span className="text-sm font-medium"> 182 {t("mobileNav.iosShortcut")} 183 </span> 184 </a> 185 </div> 186 187 <div className="h-px bg-surface-100 dark:bg-surface-700 my-1 mx-3" /> 188 189 <button 190 className="w-full flex items-center gap-2.5 p-3 rounded-xl hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors text-red-500 dark:text-red-400" 191 onClick={() => { 192 logout(); 193 closeMenu(); 194 }} 195 > 196 <LogOut size={16} className="shrink-0" /> 197 <span className="text-sm font-medium"> 198 {t("nav.logOut")} 199 </span> 200 </button> 201 </> 202 ) : ( 203 <> 204 <a 205 href="/login" 206 className="flex items-center gap-2.5 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors text-surface-700 dark:text-surface-200" 207 onClick={closeMenu} 208 > 209 <User size={16} className="shrink-0" /> 210 <span className="text-sm font-medium"> 211 {t("nav.signIn")} 212 </span> 213 </a> 214 {[ 215 { 216 href: "/collections", 217 icon: Folder, 218 label: t("nav.collections"), 219 }, 220 { 221 href: "/settings", 222 icon: Settings, 223 label: t("nav.settings"), 224 }, 225 ].map(({ href, icon: Icon, label }) => ( 226 <a 227 key={href} 228 href={href} 229 className="flex items-center gap-2.5 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors text-surface-700 dark:text-surface-200" 230 onClick={(e) => { 231 if (onNavigate) { 232 e.preventDefault(); 233 onNavigate(href); 234 } 235 closeMenu(); 236 }} 237 > 238 <Icon size={16} className="shrink-0" /> 239 <span className="text-sm font-medium">{label}</span> 240 </a> 241 ))} 242 243 <div className="h-px bg-surface-100 dark:bg-surface-700 my-1 mx-3" /> 244 245 <a 246 href="https://www.icloud.com/shortcuts/1e33ebf52f55431fae1e187cfe9738c3" 247 target="_blank" 248 rel="noopener noreferrer" 249 className="flex items-center gap-2.5 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors text-surface-700 dark:text-surface-200" 250 onClick={closeMenu} 251 > 252 <AppleIcon size={16} /> 253 <span className="text-sm font-medium"> 254 {t("mobileNav.iosShortcut")} 255 </span> 256 </a> 257 </> 258 )} 259 </div> 260 </div> 261 </div> 262 )} 263 264 <nav 265 className="fixed bottom-0 left-0 right-0 bg-white/95 dark:bg-surface-900/95 backdrop-blur-md border-t border-surface-200 dark:border-surface-800 flex items-center justify-around z-50 md:hidden" 266 style={{ 267 height: "calc(3.5rem + env(safe-area-inset-bottom))", 268 paddingBottom: "env(safe-area-inset-bottom)", 269 }} 270 > 271 <a 272 href="/home" 273 className="flex flex-col items-center justify-center w-14 h-14 gap-0.5 transition-colors" 274 onClick={(e) => { 275 if (onNavigate) { 276 e.preventDefault(); 277 onNavigate("/home"); 278 } 279 setCurrentPath("/home"); 280 closeMenu(); 281 }} 282 > 283 <div 284 className={`p-2 rounded-xl transition-colors ${ 285 isActive("/home") 286 ? "bg-primary-50 dark:bg-primary-950/50 text-primary-600 dark:text-primary-400" 287 : "text-surface-400 dark:text-surface-500" 288 }`} 289 > 290 <Home size={22} strokeWidth={isActive("/home") ? 2 : 1.5} /> 291 </div> 292 </a> 293 294 <a 295 href="/search" 296 className="flex flex-col items-center justify-center w-14 h-14 gap-0.5 transition-colors" 297 onClick={(e) => { 298 if (onNavigate) { 299 e.preventDefault(); 300 onNavigate("/search"); 301 } 302 setCurrentPath("/search"); 303 closeMenu(); 304 }} 305 > 306 <div 307 className={`p-2 rounded-xl transition-colors ${ 308 isActive("/search") 309 ? "bg-primary-50 dark:bg-primary-950/50 text-primary-600 dark:text-primary-400" 310 : "text-surface-400 dark:text-surface-500" 311 }`} 312 > 313 <Search size={22} strokeWidth={isActive("/search") ? 2 : 1.5} /> 314 </div> 315 </a> 316 317 {isAuthenticated ? ( 318 <a 319 href="/new" 320 className="flex items-center justify-center w-11 h-11 rounded-2xl bg-primary-600 dark:bg-primary-600 text-white shadow-md active:scale-95 transition-transform" 321 onClick={(e) => { 322 if (onNavigate) { 323 e.preventDefault(); 324 onNavigate("/new"); 325 } 326 setCurrentPath("/new"); 327 closeMenu(); 328 }} 329 > 330 <PenSquare size={18} strokeWidth={2} /> 331 </a> 332 ) : ( 333 <a 334 href="/login" 335 className="flex items-center justify-center w-11 h-11 rounded-2xl bg-primary-600 text-white shadow-md active:scale-95 transition-transform" 336 onClick={closeMenu} 337 > 338 <User size={18} strokeWidth={2} /> 339 </a> 340 )} 341 342 {isAuthenticated ? ( 343 <a 344 href="/notifications" 345 className="flex flex-col items-center justify-center w-14 h-14 gap-0.5 relative transition-colors" 346 onClick={(e) => { 347 if (onNavigate) { 348 e.preventDefault(); 349 onNavigate("/notifications"); 350 } 351 setCurrentPath("/notifications"); 352 closeMenu(); 353 }} 354 > 355 <div 356 className={`p-2 rounded-xl transition-colors relative ${ 357 isActive("/notifications") 358 ? "bg-primary-50 dark:bg-primary-950/50 text-primary-600 dark:text-primary-400" 359 : "text-surface-400 dark:text-surface-500" 360 }`} 361 > 362 <Bell 363 size={22} 364 strokeWidth={isActive("/notifications") ? 2 : 1.5} 365 /> 366 {unreadCount > 0 && ( 367 <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full ring-2 ring-white dark:ring-surface-900" /> 368 )} 369 </div> 370 </a> 371 ) : ( 372 <div className="w-14" /> 373 )} 374 375 <button 376 className="flex flex-col items-center justify-center w-14 h-14 gap-0.5 transition-colors" 377 onClick={() => setIsMenuOpen(!isMenuOpen)} 378 > 379 <div 380 className={`p-2 rounded-xl transition-colors ${ 381 isMenuOpen 382 ? "bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-200" 383 : "text-surface-400 dark:text-surface-500" 384 }`} 385 > 386 {isMenuOpen ? ( 387 <X size={22} strokeWidth={1.5} /> 388 ) : ( 389 <svg 390 width="22" 391 height="22" 392 viewBox="0 0 22 22" 393 fill="none" 394 xmlns="http://www.w3.org/2000/svg" 395 > 396 <circle cx="4" cy="11" r="1.75" fill="currentColor" /> 397 <circle cx="11" cy="11" r="1.75" fill="currentColor" /> 398 <circle cx="18" cy="11" r="1.75" fill="currentColor" /> 399 </svg> 400 )} 401 </div> 402 </button> 403 </nav> 404 </> 405 ); 406}