Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

enhance mobile nav

+337 -65
+219 -61
web/src/components/MobileNav.jsx
··· 1 1 import { Link, useLocation } from "react-router-dom"; 2 2 import { useAuth } from "../context/AuthContext"; 3 - import { Home, Search, Folder, User, PenSquare, Bookmark } from "lucide-react"; 3 + import { useState, useEffect } from "react"; 4 + import { getUnreadNotificationCount } from "../api/client"; 5 + import { 6 + Home, 7 + Search, 8 + Folder, 9 + User, 10 + PenSquare, 11 + Bookmark, 12 + Settings, 13 + MoreHorizontal, 14 + LogOut, 15 + Bell, 16 + Highlighter, 17 + } from "lucide-react"; 4 18 5 19 export default function MobileNav() { 6 - const { user, isAuthenticated } = useAuth(); 20 + const { user, isAuthenticated, logout } = useAuth(); 7 21 const location = useLocation(); 22 + const [isMenuOpen, setIsMenuOpen] = useState(false); 23 + const [unreadCount, setUnreadCount] = useState(0); 8 24 9 25 const isActive = (path) => { 10 26 if (path === "/") return location.pathname === "/"; 11 27 return location.pathname.startsWith(path); 12 28 }; 13 29 14 - return ( 15 - <nav className="mobile-bottom-nav"> 16 - <Link 17 - to="/home" 18 - className={`mobile-bottom-nav-item ${isActive("/home") ? "active" : ""}`} 19 - > 20 - <Home size={22} /> 21 - <span>Home</span> 22 - </Link> 30 + useEffect(() => { 31 + if (isAuthenticated) { 32 + getUnreadNotificationCount() 33 + .then((data) => setUnreadCount(data.count || 0)) 34 + .catch(() => {}); 35 + } 36 + }, [isAuthenticated]); 23 37 24 - <Link 25 - to="/url" 26 - className={`mobile-bottom-nav-item ${isActive("/url") ? "active" : ""}`} 27 - > 28 - <Search size={22} /> 29 - <span>Browse</span> 30 - </Link> 38 + const closeMenu = () => setIsMenuOpen(false); 31 39 32 - {isAuthenticated ? ( 40 + return ( 41 + <> 42 + {isMenuOpen && ( 33 43 <> 34 - <Link 35 - to="/new" 36 - className="mobile-bottom-nav-item mobile-bottom-nav-new" 37 - > 38 - <div className="mobile-nav-new-btn"> 39 - <PenSquare size={20} /> 40 - </div> 41 - </Link> 44 + <div className="mobile-nav-overlay" onClick={closeMenu} /> 45 + <div className="mobile-nav-menu"> 46 + {isAuthenticated ? ( 47 + <> 48 + <Link 49 + to={`/profile/${user.did}`} 50 + className="mobile-menu-profile-card" 51 + onClick={closeMenu} 52 + > 53 + {user.avatar ? ( 54 + <img 55 + src={user.avatar} 56 + alt="" 57 + className="mobile-nav-avatar" 58 + /> 59 + ) : ( 60 + <div 61 + className="mobile-nav-avatar" 62 + style={{ 63 + background: "var(--bg-secondary)", 64 + display: "flex", 65 + alignItems: "center", 66 + justifyContent: "center", 67 + }} 68 + > 69 + <User size={14} /> 70 + </div> 71 + )} 72 + <div style={{ display: "flex", flexDirection: "column" }}> 73 + <span 74 + style={{ 75 + fontWeight: 600, 76 + fontSize: "0.9rem", 77 + color: "var(--text-primary)", 78 + }} 79 + > 80 + {user.displayName || user.handle} 81 + </span> 82 + <span 83 + style={{ 84 + fontSize: "0.8rem", 85 + color: "var(--text-tertiary)", 86 + }} 87 + > 88 + @{user.handle} 89 + </span> 90 + </div> 91 + </Link> 42 92 43 - <Link 44 - to="/bookmarks" 45 - className={`mobile-bottom-nav-item ${isActive("/bookmarks") || isActive("/collections") ? "active" : ""}`} 46 - > 47 - <Bookmark size={22} /> 48 - <span>Library</span> 49 - </Link> 93 + <Link 94 + to="/highlights" 95 + className="mobile-menu-item" 96 + onClick={closeMenu} 97 + > 98 + <Highlighter size={20} /> 99 + <span>Highlights</span> 100 + </Link> 101 + 102 + <Link 103 + to="/bookmarks" 104 + className="mobile-menu-item" 105 + onClick={closeMenu} 106 + > 107 + <Bookmark size={20} /> 108 + <span>Bookmarks</span> 109 + </Link> 50 110 51 - <Link 52 - to={user?.did ? `/profile/${user.did}` : "/profile"} 53 - className={`mobile-bottom-nav-item ${isActive("/profile") ? "active" : ""}`} 54 - > 55 - {user?.avatar ? ( 56 - <img src={user.avatar} alt="" className="mobile-nav-avatar" /> 111 + <Link 112 + to="/collections" 113 + className="mobile-menu-item" 114 + onClick={closeMenu} 115 + > 116 + <Folder size={20} /> 117 + <span>Collections</span> 118 + </Link> 119 + 120 + <Link 121 + to="/settings" 122 + className="mobile-menu-item" 123 + onClick={closeMenu} 124 + > 125 + <Settings size={20} /> 126 + <span>Settings</span> 127 + </Link> 128 + 129 + <div className="dropdown-divider" /> 130 + 131 + <button 132 + className="mobile-menu-item danger" 133 + onClick={() => { 134 + logout(); 135 + closeMenu(); 136 + }} 137 + > 138 + <LogOut size={20} /> 139 + <span>Log Out</span> 140 + </button> 141 + </> 57 142 ) : ( 58 - <User size={22} /> 143 + <> 144 + <Link 145 + to="/login" 146 + className="mobile-menu-item" 147 + onClick={closeMenu} 148 + > 149 + <User size={20} /> 150 + <span>Sign In</span> 151 + </Link> 152 + <Link 153 + to="/collections" 154 + className="mobile-menu-item" 155 + onClick={closeMenu} 156 + > 157 + <Folder size={20} /> 158 + <span>Collections</span> 159 + </Link> 160 + <Link 161 + to="/settings" 162 + className="mobile-menu-item" 163 + onClick={closeMenu} 164 + > 165 + <Settings size={20} /> 166 + <span>Settings</span> 167 + </Link> 168 + </> 59 169 )} 60 - <span>You</span> 61 - </Link> 170 + </div> 62 171 </> 63 - ) : ( 64 - <> 172 + )} 173 + 174 + <nav className="mobile-bottom-nav"> 175 + <Link 176 + to="/home" 177 + className={`mobile-bottom-nav-item ${isActive("/home") ? "active" : ""}`} 178 + onClick={closeMenu} 179 + > 180 + <Home size={24} strokeWidth={1.5} /> 181 + </Link> 182 + 183 + <Link 184 + to="/url" 185 + className={`mobile-bottom-nav-item ${isActive("/url") ? "active" : ""}`} 186 + onClick={closeMenu} 187 + > 188 + <Search size={24} strokeWidth={1.5} /> 189 + </Link> 190 + 191 + {isAuthenticated ? ( 192 + <> 193 + <Link 194 + to="/new" 195 + className="mobile-bottom-nav-item mobile-bottom-nav-new" 196 + onClick={closeMenu} 197 + > 198 + <div className="mobile-nav-new-btn"> 199 + <PenSquare size={20} strokeWidth={2} /> 200 + </div> 201 + </Link> 202 + 203 + <Link 204 + to="/notifications" 205 + className={`mobile-bottom-nav-item ${isActive("/notifications") ? "active" : ""}`} 206 + onClick={closeMenu} 207 + > 208 + <div style={{ position: "relative", display: "flex" }}> 209 + <Bell size={24} strokeWidth={1.5} /> 210 + {unreadCount > 0 && ( 211 + <span 212 + style={{ 213 + position: "absolute", 214 + top: -2, 215 + right: -2, 216 + width: 8, 217 + height: 8, 218 + background: "var(--accent)", 219 + borderRadius: "50%", 220 + border: "2px solid var(--nav-bg)", 221 + }} 222 + /> 223 + )} 224 + </div> 225 + </Link> 226 + </> 227 + ) : ( 65 228 <Link 66 229 to="/login" 67 230 className="mobile-bottom-nav-item mobile-bottom-nav-new" 231 + onClick={closeMenu} 68 232 > 69 233 <div className="mobile-nav-new-btn"> 70 - <User size={20} /> 234 + <User size={20} strokeWidth={2} /> 71 235 </div> 72 236 </Link> 237 + )} 73 238 74 - <Link 75 - to="/collections" 76 - className={`mobile-bottom-nav-item ${isActive("/collections") ? "active" : ""}`} 77 - > 78 - <Folder size={22} /> 79 - <span>Library</span> 80 - </Link> 81 - 82 - <Link to="/login" className={`mobile-bottom-nav-item`}> 83 - <User size={22} /> 84 - <span>Sign In</span> 85 - </Link> 86 - </> 87 - )} 88 - </nav> 239 + <button 240 + className={`mobile-bottom-nav-item ${isMenuOpen ? "active" : ""}`} 241 + onClick={() => setIsMenuOpen(!isMenuOpen)} 242 + > 243 + <MoreHorizontal size={24} strokeWidth={1.5} /> 244 + </button> 245 + </nav> 246 + </> 89 247 ); 90 248 }
+112 -3
web/src/css/layout.css
··· 435 435 436 436 .mobile-bottom-nav-item { 437 437 display: flex; 438 + flex: 1; 438 439 flex-direction: column; 439 440 align-items: center; 441 + justify-content: center; 440 442 gap: 4px; 441 - padding: 6px 12px; 443 + padding: 6px 0; 442 444 color: var(--text-tertiary); 443 445 text-decoration: none; 444 446 font-size: 0.65rem; 445 447 font-weight: 500; 446 448 transition: color 0.15s; 447 - min-width: 56px; 449 + min-width: 0; 448 450 } 449 451 450 452 .mobile-bottom-nav-item.active { ··· 456 458 } 457 459 458 460 .mobile-bottom-nav-new { 459 - padding: 6px 16px; 461 + padding: 6px 0; 460 462 } 461 463 462 464 .mobile-nav-new-btn { ··· 590 592 font-size: 0.85rem; 591 593 } 592 594 } 595 + 596 + .mobile-nav-overlay { 597 + position: fixed; 598 + inset: 0; 599 + background: rgba(0, 0, 0, 0.5); 600 + z-index: 150; 601 + backdrop-filter: blur(2px); 602 + -webkit-backdrop-filter: blur(2px); 603 + animation: fadeIn 0.15s ease-out; 604 + } 605 + 606 + .mobile-nav-menu { 607 + position: fixed; 608 + bottom: calc(70px + env(safe-area-inset-bottom)); 609 + right: 12px; 610 + width: 250px; 611 + background: var(--bg-elevated); 612 + border: 1px solid var(--border); 613 + border-radius: var(--radius-xl); 614 + padding: 6px; 615 + z-index: 151; 616 + box-shadow: var(--shadow-2xl); 617 + animation: mobileMenuSlide 0.2s cubic-bezier(0.16, 1, 0.3, 1); 618 + display: flex; 619 + flex-direction: column; 620 + gap: 2px; 621 + } 622 + 623 + @keyframes mobileMenuSlide { 624 + from { 625 + opacity: 0; 626 + transform: translateY(20px) scale(0.95); 627 + } 628 + 629 + to { 630 + opacity: 1; 631 + transform: translateY(0) scale(1); 632 + } 633 + } 634 + 635 + .mobile-menu-item { 636 + display: flex; 637 + align-items: center; 638 + gap: 12px; 639 + padding: 12px 14px; 640 + color: var(--text-secondary); 641 + text-decoration: none; 642 + font-size: 0.95rem; 643 + font-weight: 500; 644 + border-radius: var(--radius-md); 645 + transition: all 0.1s; 646 + background: transparent; 647 + border: none; 648 + width: 100%; 649 + text-align: left; 650 + cursor: pointer; 651 + } 652 + 653 + .mobile-menu-item:hover, 654 + .mobile-menu-item:active { 655 + background: var(--bg-hover); 656 + color: var(--text-primary); 657 + } 658 + 659 + .mobile-menu-item svg { 660 + opacity: 0.8; 661 + } 662 + 663 + .mobile-menu-item.active { 664 + background: var(--bg-tertiary); 665 + color: var(--accent); 666 + } 667 + 668 + .mobile-menu-item.danger { 669 + color: var(--error); 670 + } 671 + 672 + .mobile-menu-item.danger:hover { 673 + background: rgba(239, 68, 68, 0.1); 674 + } 675 + 676 + .mobile-menu-profile-card { 677 + display: flex; 678 + align-items: center; 679 + gap: 12px; 680 + padding: 12px; 681 + background: var(--bg-tertiary); 682 + border-radius: var(--radius-lg); 683 + margin-bottom: 6px; 684 + text-decoration: none; 685 + border: 1px solid transparent; 686 + } 687 + 688 + .mobile-menu-profile-card:active { 689 + background: var(--bg-hover); 690 + transform: scale(0.98); 691 + } 692 + 693 + .mobile-menu-badge { 694 + margin-left: auto; 695 + background: var(--accent); 696 + color: white; 697 + font-size: 0.75rem; 698 + font-weight: 700; 699 + padding: 2px 8px; 700 + border-radius: 99px; 701 + }
+6 -1
web/src/pages/Settings.jsx
··· 86 86 <h1 className="page-title">Settings</h1> 87 87 <p className="page-description">Manage your preferences and API keys.</p> 88 88 89 - <div className="settings-section"> 89 + <div className="settings-section layout-settings-section"> 90 90 <h2>Layout</h2> 91 91 <div className="layout-options"> 92 92 <button ··· 331 331 @media (max-width: 600px) { 332 332 .layout-options { 333 333 grid-template-columns: 1fr; 334 + } 335 + } 336 + @media (max-width: 768px) { 337 + .layout-settings-section { 338 + display: none; 334 339 } 335 340 } 336 341 `}</style>