Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 212 lines 7.9 kB view raw
1import React, { useEffect, useState } from "react"; 2import { 3 Home, 4 Bookmark, 5 Settings, 6 LogOut, 7 Bell, 8 Sun, 9 Moon, 10 Monitor, 11 Folder, 12 LogIn, 13 PenSquare, 14 MessageSquareText, 15 Highlighter, 16 Compass, 17} from "lucide-react"; 18import { useStore } from "@nanostores/react"; 19import { $user, logout } from "../../store/auth"; 20import { $theme, cycleTheme } from "../../store/theme"; 21import { getUnreadNotificationCount } from "../../api/client"; 22import { Link, useLocation } from "react-router-dom"; 23import { Avatar, CountBadge } from "../ui"; 24 25export default function Sidebar() { 26 const user = useStore($user); 27 const theme = useStore($theme); 28 const location = useLocation(); 29 const currentPath = location.pathname; 30 const [unreadCount, setUnreadCount] = useState(0); 31 32 useEffect(() => { 33 if (!user) return; 34 35 const checkNotifications = async () => { 36 const count = await getUnreadNotificationCount(); 37 setUnreadCount(count); 38 }; 39 40 checkNotifications(); 41 const interval = setInterval(checkNotifications, 30000); 42 return () => clearInterval(interval); 43 }, [user]); 44 45 const publicNavItems = [ 46 { icon: Home, label: "Feed", href: "/home", badge: undefined }, 47 { icon: Compass, label: "Discover", href: "/discover", badge: undefined }, 48 { 49 icon: MessageSquareText, 50 label: "Annotations", 51 href: "/annotations", 52 badge: undefined, 53 }, 54 { 55 icon: Highlighter, 56 label: "Highlights", 57 href: "/highlights", 58 badge: undefined, 59 }, 60 { 61 icon: Bookmark, 62 label: "Bookmarks", 63 href: "/bookmarks", 64 badge: undefined, 65 }, 66 ]; 67 68 const authNavItems = [ 69 { icon: Home, label: "Feed", href: "/home" }, 70 { icon: Compass, label: "Discover", href: "/discover" }, 71 { 72 icon: Bell, 73 label: "Activity", 74 href: "/notifications", 75 badge: unreadCount, 76 }, 77 { icon: MessageSquareText, label: "Annotations", href: "/annotations" }, 78 { icon: Highlighter, label: "Highlights", href: "/highlights" }, 79 { icon: Bookmark, label: "Bookmarks", href: "/bookmarks" }, 80 { icon: Folder, label: "Collections", href: "/collections" }, 81 ]; 82 83 const navItems = user ? authNavItems : publicNavItems; 84 85 return ( 86 <aside className="sticky top-0 h-screen hidden md:flex flex-col justify-between py-6 px-2 lg:px-4 z-50 w-[68px] lg:w-[260px] transition-all duration-200"> 87 <div className="flex flex-col gap-6"> 88 <Link 89 to="/home" 90 className="px-3 hover:opacity-80 transition-opacity w-fit flex items-center gap-2.5" 91 > 92 <img src="/logo.svg" alt="Margin" className="w-8 h-8" /> 93 </Link> 94 95 <nav className="flex flex-col gap-0.5"> 96 {navItems.map((item) => { 97 const isActive = 98 currentPath === item.href || 99 (item.href !== "/home" && currentPath.startsWith(item.href)); 100 return ( 101 <Link 102 key={item.href} 103 to={item.href} 104 title={item.label} 105 className={`flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg transition-all duration-150 text-[14px] group ${ 106 isActive 107 ? "font-semibold text-primary-700 dark:text-primary-300 bg-primary-50 dark:bg-primary-950/40" 108 : "font-medium text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-white" 109 }`} 110 > 111 <item.icon 112 size={20} 113 className={`transition-colors ${isActive ? "text-primary-600 dark:text-primary-400" : ""}`} 114 strokeWidth={isActive ? 2.25 : 1.75} 115 /> 116 <span className="flex-1 hidden lg:inline">{item.label}</span> 117 {(item.badge ?? 0) > 0 && ( 118 <CountBadge count={item.badge ?? 0} /> 119 )} 120 </Link> 121 ); 122 })} 123 124 {user && ( 125 <Link 126 to="/new" 127 title="New annotation" 128 className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 mt-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white hover:bg-primary-700 dark:hover:bg-primary-400 transition-colors text-[14px] font-semibold" 129 > 130 <PenSquare size={20} strokeWidth={1.75} /> 131 <span className="hidden lg:inline">New</span> 132 </Link> 133 )} 134 </nav> 135 </div> 136 137 <div className="space-y-1"> 138 <button 139 onClick={cycleTheme} 140 title={ 141 theme === "light" ? "Light" : theme === "dark" ? "Dark" : "System" 142 } 143 className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800 text-[13px] font-medium text-surface-500 dark:text-surface-400 w-full transition-colors" 144 > 145 {theme === "light" ? ( 146 <Sun size={18} /> 147 ) : theme === "dark" ? ( 148 <Moon size={18} /> 149 ) : ( 150 <Monitor size={18} /> 151 )} 152 <span className="hidden lg:inline"> 153 {theme === "light" ? "Light" : theme === "dark" ? "Dark" : "System"} 154 </span> 155 </button> 156 157 {user ? ( 158 <> 159 <Link 160 to="/settings" 161 title="Settings" 162 className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800 text-[13px] font-medium text-surface-500 dark:text-surface-400 transition-colors" 163 > 164 <Settings size={18} /> 165 <span className="hidden lg:inline">Settings</span> 166 </Link> 167 168 <div className="h-px bg-surface-200/60 dark:bg-surface-800/60 my-2" /> 169 170 <Link 171 to={`/profile/${user.did}`} 172 title={user.displayName || user.handle} 173 className="flex items-center justify-center lg:justify-start gap-2.5 p-2 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors w-full" 174 > 175 <Avatar did={user.did} avatar={user.avatar} size="sm" /> 176 <div className="flex-1 min-w-0 hidden lg:block"> 177 <p className="font-medium text-surface-900 dark:text-white truncate text-[13px]"> 178 {user.displayName || user.handle} 179 </p> 180 <p className="text-[11px] text-surface-500 dark:text-surface-400 truncate"> 181 @{user.handle} 182 </p> 183 </div> 184 </Link> 185 186 <button 187 onClick={logout} 188 title="Log out" 189 className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-[13px] font-medium text-surface-400 dark:text-surface-500 hover:text-red-600 dark:hover:text-red-400 w-full text-left transition-colors" 190 > 191 <LogOut size={16} /> 192 <span className="hidden lg:inline">Log out</span> 193 </button> 194 </> 195 ) : ( 196 <> 197 <div className="h-px bg-surface-200/60 dark:bg-surface-800/60 my-2" /> 198 199 <Link 200 to="/login" 201 title="Sign in" 202 className="flex items-center justify-center lg:justify-start gap-3 px-0 lg:px-3 py-2.5 rounded-lg bg-primary-50 dark:bg-primary-950/40 text-primary-700 dark:text-primary-300 hover:bg-primary-100 dark:hover:bg-primary-950/60 text-[13px] font-semibold transition-colors" 203 > 204 <LogIn size={18} /> 205 <span className="hidden lg:inline">Sign in</span> 206 </Link> 207 </> 208 )} 209 </div> 210 </aside> 211 ); 212}