Write on the margins of 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 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}