(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 272 lines 9.4 kB view raw
1import { 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 { Avatar, CountBadge } from "../ui"; 23import { useTranslation } from "react-i18next"; 24 25interface SidebarProps { 26 currentPath?: string; 27 onNavigate?: (path: string) => void; 28} 29 30export default function Sidebar({ 31 currentPath: initialPath, 32 onNavigate, 33}: SidebarProps) { 34 const { t } = useTranslation(); 35 const user = useStore($user); 36 const theme = useStore($theme); 37 const currentPath = initialPath || "/"; 38 const [unreadCount, setUnreadCount] = useState(0); 39 40 useEffect(() => { 41 if (!user) return; 42 43 const checkNotifications = async () => { 44 const count = await getUnreadNotificationCount(); 45 setUnreadCount(count); 46 }; 47 48 checkNotifications(); 49 const interval = setInterval(checkNotifications, 30000); 50 return () => clearInterval(interval); 51 }, [user]); 52 53 const publicNavItems = [ 54 { icon: Home, label: t("nav.feed"), href: "/home", badge: undefined }, 55 { 56 icon: Compass, 57 label: t("nav.discover"), 58 href: "/discover", 59 badge: undefined, 60 }, 61 { 62 icon: MessageSquareText, 63 label: t("nav.annotations"), 64 href: "/annotations", 65 badge: undefined, 66 }, 67 { 68 icon: Highlighter, 69 label: t("nav.highlights"), 70 href: "/highlights", 71 badge: undefined, 72 }, 73 { 74 icon: Bookmark, 75 label: t("nav.bookmarks"), 76 href: "/bookmarks", 77 badge: undefined, 78 }, 79 ]; 80 81 const authNavItems = [ 82 { icon: Home, label: t("nav.feed"), href: "/home" }, 83 { icon: Compass, label: t("nav.discover"), href: "/discover" }, 84 { 85 icon: Bell, 86 label: t("nav.activity"), 87 href: "/notifications", 88 badge: unreadCount, 89 }, 90 { 91 icon: MessageSquareText, 92 label: t("nav.annotations"), 93 href: "/annotations", 94 }, 95 { icon: Highlighter, label: t("nav.highlights"), href: "/highlights" }, 96 { icon: Bookmark, label: t("nav.bookmarks"), href: "/bookmarks" }, 97 { icon: Folder, label: t("nav.collections"), href: "/collections" }, 98 ]; 99 100 const navItems = user ? authNavItems : publicNavItems; 101 102 const themeLabel = 103 theme === "light" 104 ? t("nav.themeLight") 105 : theme === "dark" 106 ? t("nav.themeDark") 107 : t("nav.themeSystem"); 108 109 return ( 110 <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"> 111 <div className="flex flex-col gap-6"> 112 <a 113 href="/home" 114 onClick={ 115 onNavigate 116 ? (e) => { 117 e.preventDefault(); 118 onNavigate("/home"); 119 } 120 : undefined 121 } 122 className="px-3 hover:opacity-80 transition-opacity w-fit flex items-center gap-2.5" 123 > 124 <img src="/logo.svg" alt="Margin" className="w-8 h-8" /> 125 </a> 126 127 <nav className="flex flex-col gap-0.5"> 128 {navItems.map((item) => { 129 const isActive = 130 currentPath === item.href || 131 (item.href !== "/home" && currentPath.startsWith(item.href)); 132 return ( 133 <a 134 key={item.href} 135 href={item.href} 136 title={item.label} 137 onClick={ 138 onNavigate 139 ? (e) => { 140 e.preventDefault(); 141 onNavigate(item.href); 142 } 143 : undefined 144 } 145 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 ${ 146 isActive 147 ? "font-semibold text-primary-700 dark:text-primary-300 bg-primary-50 dark:bg-primary-950/40" 148 : "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" 149 }`} 150 > 151 <item.icon 152 size={20} 153 className={`transition-colors ${isActive ? "text-primary-600 dark:text-primary-400" : ""}`} 154 strokeWidth={isActive ? 2.25 : 1.75} 155 /> 156 <span className="flex-1 hidden lg:inline">{item.label}</span> 157 {(item.badge ?? 0) > 0 && ( 158 <CountBadge count={item.badge ?? 0} /> 159 )} 160 </a> 161 ); 162 })} 163 164 {user && ( 165 <a 166 href="/new" 167 title={t("nav.new")} 168 onClick={ 169 onNavigate 170 ? (e) => { 171 e.preventDefault(); 172 onNavigate("/new"); 173 } 174 : undefined 175 } 176 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" 177 > 178 <PenSquare size={20} strokeWidth={1.75} /> 179 <span className="hidden lg:inline">{t("nav.new")}</span> 180 </a> 181 )} 182 </nav> 183 </div> 184 185 <div className="space-y-1"> 186 <button 187 onClick={cycleTheme} 188 title={themeLabel} 189 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" 190 > 191 {theme === "light" ? ( 192 <Sun size={18} /> 193 ) : theme === "dark" ? ( 194 <Moon size={18} /> 195 ) : ( 196 <Monitor size={18} /> 197 )} 198 <span className="hidden lg:inline">{themeLabel}</span> 199 </button> 200 201 {user ? ( 202 <> 203 <a 204 href="/settings" 205 title={t("nav.settings")} 206 onClick={ 207 onNavigate 208 ? (e) => { 209 e.preventDefault(); 210 onNavigate("/settings"); 211 } 212 : undefined 213 } 214 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" 215 > 216 <Settings size={18} /> 217 <span className="hidden lg:inline">{t("nav.settings")}</span> 218 </a> 219 220 <div className="h-px bg-surface-200/60 dark:bg-surface-800/60 my-2" /> 221 222 <a 223 href={`/profile/${user.did}`} 224 title={user.displayName || user.handle} 225 onClick={ 226 onNavigate 227 ? (e) => { 228 e.preventDefault(); 229 onNavigate(`/profile/${user.did}`); 230 } 231 : undefined 232 } 233 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" 234 > 235 <Avatar did={user.did} avatar={user.avatar} size="sm" /> 236 <div className="flex-1 min-w-0 hidden lg:block"> 237 <p className="font-medium text-surface-900 dark:text-white truncate text-[13px]"> 238 {user.displayName || user.handle} 239 </p> 240 <p className="text-[11px] text-surface-500 dark:text-surface-400 truncate"> 241 @{user.handle} 242 </p> 243 </div> 244 </a> 245 246 <button 247 onClick={logout} 248 title={t("nav.logOut")} 249 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" 250 > 251 <LogOut size={16} /> 252 <span className="hidden lg:inline">{t("nav.logOut")}</span> 253 </button> 254 </> 255 ) : ( 256 <> 257 <div className="h-px bg-surface-200/60 dark:bg-surface-800/60 my-2" /> 258 259 <a 260 href="/login" 261 title={t("nav.signIn")} 262 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" 263 > 264 <LogIn size={18} /> 265 <span className="hidden lg:inline">{t("nav.signIn")}</span> 266 </a> 267 </> 268 )} 269 </div> 270 </aside> 271 ); 272}