tangled
alpha
login
or
join now
margin.at
/
margin
90
fork
atom
Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
90
fork
atom
overview
issues
4
pulls
1
pipelines
enhance mobile nav
scanash.com
1 month ago
d177110f
ba77dfa9
+337
-65
3 changed files
expand all
collapse all
unified
split
web
src
components
MobileNav.jsx
css
layout.css
pages
Settings.jsx
+219
-61
web/src/components/MobileNav.jsx
···
1
import { Link, useLocation } from "react-router-dom";
2
import { useAuth } from "../context/AuthContext";
3
-
import { Home, Search, Folder, User, PenSquare, Bookmark } from "lucide-react";
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4
5
export default function MobileNav() {
6
-
const { user, isAuthenticated } = useAuth();
7
const location = useLocation();
0
0
8
9
const isActive = (path) => {
10
if (path === "/") return location.pathname === "/";
11
return location.pathname.startsWith(path);
12
};
13
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>
23
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>
31
32
-
{isAuthenticated ? (
0
0
33
<>
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>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
42
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>
0
0
0
0
0
0
0
0
0
0
50
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" />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
57
) : (
58
-
<User size={22} />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
59
)}
60
-
<span>You</span>
61
-
</Link>
62
</>
63
-
) : (
64
-
<>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
65
<Link
66
to="/login"
67
className="mobile-bottom-nav-item mobile-bottom-nav-new"
0
68
>
69
<div className="mobile-nav-new-btn">
70
-
<User size={20} />
71
</div>
72
</Link>
0
73
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>
89
);
90
}
···
1
import { Link, useLocation } from "react-router-dom";
2
import { useAuth } from "../context/AuthContext";
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";
18
19
export default function MobileNav() {
20
+
const { user, isAuthenticated, logout } = useAuth();
21
const location = useLocation();
22
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
23
+
const [unreadCount, setUnreadCount] = useState(0);
24
25
const isActive = (path) => {
26
if (path === "/") return location.pathname === "/";
27
return location.pathname.startsWith(path);
28
};
29
30
+
useEffect(() => {
31
+
if (isAuthenticated) {
32
+
getUnreadNotificationCount()
33
+
.then((data) => setUnreadCount(data.count || 0))
34
+
.catch(() => {});
35
+
}
36
+
}, [isAuthenticated]);
0
0
37
38
+
const closeMenu = () => setIsMenuOpen(false);
0
0
0
0
0
0
39
40
+
return (
41
+
<>
42
+
{isMenuOpen && (
43
<>
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>
92
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>
110
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
+
</>
142
) : (
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
+
</>
169
)}
170
+
</div>
0
171
</>
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
+
) : (
228
<Link
229
to="/login"
230
className="mobile-bottom-nav-item mobile-bottom-nav-new"
231
+
onClick={closeMenu}
232
>
233
<div className="mobile-nav-new-btn">
234
+
<User size={20} strokeWidth={2} />
235
</div>
236
</Link>
237
+
)}
238
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
+
</>
0
0
0
0
0
0
0
247
);
248
}
+112
-3
web/src/css/layout.css
···
435
436
.mobile-bottom-nav-item {
437
display: flex;
0
438
flex-direction: column;
439
align-items: center;
0
440
gap: 4px;
441
-
padding: 6px 12px;
442
color: var(--text-tertiary);
443
text-decoration: none;
444
font-size: 0.65rem;
445
font-weight: 500;
446
transition: color 0.15s;
447
-
min-width: 56px;
448
}
449
450
.mobile-bottom-nav-item.active {
···
456
}
457
458
.mobile-bottom-nav-new {
459
-
padding: 6px 16px;
460
}
461
462
.mobile-nav-new-btn {
···
590
font-size: 0.85rem;
591
}
592
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
435
436
.mobile-bottom-nav-item {
437
display: flex;
438
+
flex: 1;
439
flex-direction: column;
440
align-items: center;
441
+
justify-content: center;
442
gap: 4px;
443
+
padding: 6px 0;
444
color: var(--text-tertiary);
445
text-decoration: none;
446
font-size: 0.65rem;
447
font-weight: 500;
448
transition: color 0.15s;
449
+
min-width: 0;
450
}
451
452
.mobile-bottom-nav-item.active {
···
458
}
459
460
.mobile-bottom-nav-new {
461
+
padding: 6px 0;
462
}
463
464
.mobile-nav-new-btn {
···
592
font-size: 0.85rem;
593
}
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
<h1 className="page-title">Settings</h1>
87
<p className="page-description">Manage your preferences and API keys.</p>
88
89
-
<div className="settings-section">
90
<h2>Layout</h2>
91
<div className="layout-options">
92
<button
···
331
@media (max-width: 600px) {
332
.layout-options {
333
grid-template-columns: 1fr;
0
0
0
0
0
334
}
335
}
336
`}</style>
···
86
<h1 className="page-title">Settings</h1>
87
<p className="page-description">Manage your preferences and API keys.</p>
88
89
+
<div className="settings-section layout-settings-section">
90
<h2>Layout</h2>
91
<div className="layout-options">
92
<button
···
331
@media (max-width: 600px) {
332
.layout-options {
333
grid-template-columns: 1fr;
334
+
}
335
+
}
336
+
@media (max-width: 768px) {
337
+
.layout-settings-section {
338
+
display: none;
339
}
340
}
341
`}</style>