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