tangled
alpha
login
or
join now
margin.at
/
margin
89
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
89
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
1
import { Link, useLocation } from "react-router-dom";
2
2
import { useAuth } from "../context/AuthContext";
3
3
-
import { Home, Search, Folder, User, PenSquare, Bookmark } from "lucide-react";
3
3
+
import { useState, useEffect } from "react";
4
4
+
import { getUnreadNotificationCount } from "../api/client";
5
5
+
import {
6
6
+
Home,
7
7
+
Search,
8
8
+
Folder,
9
9
+
User,
10
10
+
PenSquare,
11
11
+
Bookmark,
12
12
+
Settings,
13
13
+
MoreHorizontal,
14
14
+
LogOut,
15
15
+
Bell,
16
16
+
Highlighter,
17
17
+
} from "lucide-react";
4
18
5
19
export default function MobileNav() {
6
6
-
const { user, isAuthenticated } = useAuth();
20
20
+
const { user, isAuthenticated, logout } = useAuth();
7
21
const location = useLocation();
22
22
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
23
23
+
const [unreadCount, setUnreadCount] = useState(0);
8
24
9
25
const isActive = (path) => {
10
26
if (path === "/") return location.pathname === "/";
11
27
return location.pathname.startsWith(path);
12
28
};
13
29
14
14
-
return (
15
15
-
<nav className="mobile-bottom-nav">
16
16
-
<Link
17
17
-
to="/home"
18
18
-
className={`mobile-bottom-nav-item ${isActive("/home") ? "active" : ""}`}
19
19
-
>
20
20
-
<Home size={22} />
21
21
-
<span>Home</span>
22
22
-
</Link>
30
30
+
useEffect(() => {
31
31
+
if (isAuthenticated) {
32
32
+
getUnreadNotificationCount()
33
33
+
.then((data) => setUnreadCount(data.count || 0))
34
34
+
.catch(() => {});
35
35
+
}
36
36
+
}, [isAuthenticated]);
23
37
24
24
-
<Link
25
25
-
to="/url"
26
26
-
className={`mobile-bottom-nav-item ${isActive("/url") ? "active" : ""}`}
27
27
-
>
28
28
-
<Search size={22} />
29
29
-
<span>Browse</span>
30
30
-
</Link>
38
38
+
const closeMenu = () => setIsMenuOpen(false);
31
39
32
32
-
{isAuthenticated ? (
40
40
+
return (
41
41
+
<>
42
42
+
{isMenuOpen && (
33
43
<>
34
34
-
<Link
35
35
-
to="/new"
36
36
-
className="mobile-bottom-nav-item mobile-bottom-nav-new"
37
37
-
>
38
38
-
<div className="mobile-nav-new-btn">
39
39
-
<PenSquare size={20} />
40
40
-
</div>
41
41
-
</Link>
44
44
+
<div className="mobile-nav-overlay" onClick={closeMenu} />
45
45
+
<div className="mobile-nav-menu">
46
46
+
{isAuthenticated ? (
47
47
+
<>
48
48
+
<Link
49
49
+
to={`/profile/${user.did}`}
50
50
+
className="mobile-menu-profile-card"
51
51
+
onClick={closeMenu}
52
52
+
>
53
53
+
{user.avatar ? (
54
54
+
<img
55
55
+
src={user.avatar}
56
56
+
alt=""
57
57
+
className="mobile-nav-avatar"
58
58
+
/>
59
59
+
) : (
60
60
+
<div
61
61
+
className="mobile-nav-avatar"
62
62
+
style={{
63
63
+
background: "var(--bg-secondary)",
64
64
+
display: "flex",
65
65
+
alignItems: "center",
66
66
+
justifyContent: "center",
67
67
+
}}
68
68
+
>
69
69
+
<User size={14} />
70
70
+
</div>
71
71
+
)}
72
72
+
<div style={{ display: "flex", flexDirection: "column" }}>
73
73
+
<span
74
74
+
style={{
75
75
+
fontWeight: 600,
76
76
+
fontSize: "0.9rem",
77
77
+
color: "var(--text-primary)",
78
78
+
}}
79
79
+
>
80
80
+
{user.displayName || user.handle}
81
81
+
</span>
82
82
+
<span
83
83
+
style={{
84
84
+
fontSize: "0.8rem",
85
85
+
color: "var(--text-tertiary)",
86
86
+
}}
87
87
+
>
88
88
+
@{user.handle}
89
89
+
</span>
90
90
+
</div>
91
91
+
</Link>
42
92
43
43
-
<Link
44
44
-
to="/bookmarks"
45
45
-
className={`mobile-bottom-nav-item ${isActive("/bookmarks") || isActive("/collections") ? "active" : ""}`}
46
46
-
>
47
47
-
<Bookmark size={22} />
48
48
-
<span>Library</span>
49
49
-
</Link>
93
93
+
<Link
94
94
+
to="/highlights"
95
95
+
className="mobile-menu-item"
96
96
+
onClick={closeMenu}
97
97
+
>
98
98
+
<Highlighter size={20} />
99
99
+
<span>Highlights</span>
100
100
+
</Link>
101
101
+
102
102
+
<Link
103
103
+
to="/bookmarks"
104
104
+
className="mobile-menu-item"
105
105
+
onClick={closeMenu}
106
106
+
>
107
107
+
<Bookmark size={20} />
108
108
+
<span>Bookmarks</span>
109
109
+
</Link>
50
110
51
51
-
<Link
52
52
-
to={user?.did ? `/profile/${user.did}` : "/profile"}
53
53
-
className={`mobile-bottom-nav-item ${isActive("/profile") ? "active" : ""}`}
54
54
-
>
55
55
-
{user?.avatar ? (
56
56
-
<img src={user.avatar} alt="" className="mobile-nav-avatar" />
111
111
+
<Link
112
112
+
to="/collections"
113
113
+
className="mobile-menu-item"
114
114
+
onClick={closeMenu}
115
115
+
>
116
116
+
<Folder size={20} />
117
117
+
<span>Collections</span>
118
118
+
</Link>
119
119
+
120
120
+
<Link
121
121
+
to="/settings"
122
122
+
className="mobile-menu-item"
123
123
+
onClick={closeMenu}
124
124
+
>
125
125
+
<Settings size={20} />
126
126
+
<span>Settings</span>
127
127
+
</Link>
128
128
+
129
129
+
<div className="dropdown-divider" />
130
130
+
131
131
+
<button
132
132
+
className="mobile-menu-item danger"
133
133
+
onClick={() => {
134
134
+
logout();
135
135
+
closeMenu();
136
136
+
}}
137
137
+
>
138
138
+
<LogOut size={20} />
139
139
+
<span>Log Out</span>
140
140
+
</button>
141
141
+
</>
57
142
) : (
58
58
-
<User size={22} />
143
143
+
<>
144
144
+
<Link
145
145
+
to="/login"
146
146
+
className="mobile-menu-item"
147
147
+
onClick={closeMenu}
148
148
+
>
149
149
+
<User size={20} />
150
150
+
<span>Sign In</span>
151
151
+
</Link>
152
152
+
<Link
153
153
+
to="/collections"
154
154
+
className="mobile-menu-item"
155
155
+
onClick={closeMenu}
156
156
+
>
157
157
+
<Folder size={20} />
158
158
+
<span>Collections</span>
159
159
+
</Link>
160
160
+
<Link
161
161
+
to="/settings"
162
162
+
className="mobile-menu-item"
163
163
+
onClick={closeMenu}
164
164
+
>
165
165
+
<Settings size={20} />
166
166
+
<span>Settings</span>
167
167
+
</Link>
168
168
+
</>
59
169
)}
60
60
-
<span>You</span>
61
61
-
</Link>
170
170
+
</div>
62
171
</>
63
63
-
) : (
64
64
-
<>
172
172
+
)}
173
173
+
174
174
+
<nav className="mobile-bottom-nav">
175
175
+
<Link
176
176
+
to="/home"
177
177
+
className={`mobile-bottom-nav-item ${isActive("/home") ? "active" : ""}`}
178
178
+
onClick={closeMenu}
179
179
+
>
180
180
+
<Home size={24} strokeWidth={1.5} />
181
181
+
</Link>
182
182
+
183
183
+
<Link
184
184
+
to="/url"
185
185
+
className={`mobile-bottom-nav-item ${isActive("/url") ? "active" : ""}`}
186
186
+
onClick={closeMenu}
187
187
+
>
188
188
+
<Search size={24} strokeWidth={1.5} />
189
189
+
</Link>
190
190
+
191
191
+
{isAuthenticated ? (
192
192
+
<>
193
193
+
<Link
194
194
+
to="/new"
195
195
+
className="mobile-bottom-nav-item mobile-bottom-nav-new"
196
196
+
onClick={closeMenu}
197
197
+
>
198
198
+
<div className="mobile-nav-new-btn">
199
199
+
<PenSquare size={20} strokeWidth={2} />
200
200
+
</div>
201
201
+
</Link>
202
202
+
203
203
+
<Link
204
204
+
to="/notifications"
205
205
+
className={`mobile-bottom-nav-item ${isActive("/notifications") ? "active" : ""}`}
206
206
+
onClick={closeMenu}
207
207
+
>
208
208
+
<div style={{ position: "relative", display: "flex" }}>
209
209
+
<Bell size={24} strokeWidth={1.5} />
210
210
+
{unreadCount > 0 && (
211
211
+
<span
212
212
+
style={{
213
213
+
position: "absolute",
214
214
+
top: -2,
215
215
+
right: -2,
216
216
+
width: 8,
217
217
+
height: 8,
218
218
+
background: "var(--accent)",
219
219
+
borderRadius: "50%",
220
220
+
border: "2px solid var(--nav-bg)",
221
221
+
}}
222
222
+
/>
223
223
+
)}
224
224
+
</div>
225
225
+
</Link>
226
226
+
</>
227
227
+
) : (
65
228
<Link
66
229
to="/login"
67
230
className="mobile-bottom-nav-item mobile-bottom-nav-new"
231
231
+
onClick={closeMenu}
68
232
>
69
233
<div className="mobile-nav-new-btn">
70
70
-
<User size={20} />
234
234
+
<User size={20} strokeWidth={2} />
71
235
</div>
72
236
</Link>
237
237
+
)}
73
238
74
74
-
<Link
75
75
-
to="/collections"
76
76
-
className={`mobile-bottom-nav-item ${isActive("/collections") ? "active" : ""}`}
77
77
-
>
78
78
-
<Folder size={22} />
79
79
-
<span>Library</span>
80
80
-
</Link>
81
81
-
82
82
-
<Link to="/login" className={`mobile-bottom-nav-item`}>
83
83
-
<User size={22} />
84
84
-
<span>Sign In</span>
85
85
-
</Link>
86
86
-
</>
87
87
-
)}
88
88
-
</nav>
239
239
+
<button
240
240
+
className={`mobile-bottom-nav-item ${isMenuOpen ? "active" : ""}`}
241
241
+
onClick={() => setIsMenuOpen(!isMenuOpen)}
242
242
+
>
243
243
+
<MoreHorizontal size={24} strokeWidth={1.5} />
244
244
+
</button>
245
245
+
</nav>
246
246
+
</>
89
247
);
90
248
}
+112
-3
web/src/css/layout.css
···
435
435
436
436
.mobile-bottom-nav-item {
437
437
display: flex;
438
438
+
flex: 1;
438
439
flex-direction: column;
439
440
align-items: center;
441
441
+
justify-content: center;
440
442
gap: 4px;
441
441
-
padding: 6px 12px;
443
443
+
padding: 6px 0;
442
444
color: var(--text-tertiary);
443
445
text-decoration: none;
444
446
font-size: 0.65rem;
445
447
font-weight: 500;
446
448
transition: color 0.15s;
447
447
-
min-width: 56px;
449
449
+
min-width: 0;
448
450
}
449
451
450
452
.mobile-bottom-nav-item.active {
···
456
458
}
457
459
458
460
.mobile-bottom-nav-new {
459
459
-
padding: 6px 16px;
461
461
+
padding: 6px 0;
460
462
}
461
463
462
464
.mobile-nav-new-btn {
···
590
592
font-size: 0.85rem;
591
593
}
592
594
}
595
595
+
596
596
+
.mobile-nav-overlay {
597
597
+
position: fixed;
598
598
+
inset: 0;
599
599
+
background: rgba(0, 0, 0, 0.5);
600
600
+
z-index: 150;
601
601
+
backdrop-filter: blur(2px);
602
602
+
-webkit-backdrop-filter: blur(2px);
603
603
+
animation: fadeIn 0.15s ease-out;
604
604
+
}
605
605
+
606
606
+
.mobile-nav-menu {
607
607
+
position: fixed;
608
608
+
bottom: calc(70px + env(safe-area-inset-bottom));
609
609
+
right: 12px;
610
610
+
width: 250px;
611
611
+
background: var(--bg-elevated);
612
612
+
border: 1px solid var(--border);
613
613
+
border-radius: var(--radius-xl);
614
614
+
padding: 6px;
615
615
+
z-index: 151;
616
616
+
box-shadow: var(--shadow-2xl);
617
617
+
animation: mobileMenuSlide 0.2s cubic-bezier(0.16, 1, 0.3, 1);
618
618
+
display: flex;
619
619
+
flex-direction: column;
620
620
+
gap: 2px;
621
621
+
}
622
622
+
623
623
+
@keyframes mobileMenuSlide {
624
624
+
from {
625
625
+
opacity: 0;
626
626
+
transform: translateY(20px) scale(0.95);
627
627
+
}
628
628
+
629
629
+
to {
630
630
+
opacity: 1;
631
631
+
transform: translateY(0) scale(1);
632
632
+
}
633
633
+
}
634
634
+
635
635
+
.mobile-menu-item {
636
636
+
display: flex;
637
637
+
align-items: center;
638
638
+
gap: 12px;
639
639
+
padding: 12px 14px;
640
640
+
color: var(--text-secondary);
641
641
+
text-decoration: none;
642
642
+
font-size: 0.95rem;
643
643
+
font-weight: 500;
644
644
+
border-radius: var(--radius-md);
645
645
+
transition: all 0.1s;
646
646
+
background: transparent;
647
647
+
border: none;
648
648
+
width: 100%;
649
649
+
text-align: left;
650
650
+
cursor: pointer;
651
651
+
}
652
652
+
653
653
+
.mobile-menu-item:hover,
654
654
+
.mobile-menu-item:active {
655
655
+
background: var(--bg-hover);
656
656
+
color: var(--text-primary);
657
657
+
}
658
658
+
659
659
+
.mobile-menu-item svg {
660
660
+
opacity: 0.8;
661
661
+
}
662
662
+
663
663
+
.mobile-menu-item.active {
664
664
+
background: var(--bg-tertiary);
665
665
+
color: var(--accent);
666
666
+
}
667
667
+
668
668
+
.mobile-menu-item.danger {
669
669
+
color: var(--error);
670
670
+
}
671
671
+
672
672
+
.mobile-menu-item.danger:hover {
673
673
+
background: rgba(239, 68, 68, 0.1);
674
674
+
}
675
675
+
676
676
+
.mobile-menu-profile-card {
677
677
+
display: flex;
678
678
+
align-items: center;
679
679
+
gap: 12px;
680
680
+
padding: 12px;
681
681
+
background: var(--bg-tertiary);
682
682
+
border-radius: var(--radius-lg);
683
683
+
margin-bottom: 6px;
684
684
+
text-decoration: none;
685
685
+
border: 1px solid transparent;
686
686
+
}
687
687
+
688
688
+
.mobile-menu-profile-card:active {
689
689
+
background: var(--bg-hover);
690
690
+
transform: scale(0.98);
691
691
+
}
692
692
+
693
693
+
.mobile-menu-badge {
694
694
+
margin-left: auto;
695
695
+
background: var(--accent);
696
696
+
color: white;
697
697
+
font-size: 0.75rem;
698
698
+
font-weight: 700;
699
699
+
padding: 2px 8px;
700
700
+
border-radius: 99px;
701
701
+
}
+6
-1
web/src/pages/Settings.jsx
···
86
86
<h1 className="page-title">Settings</h1>
87
87
<p className="page-description">Manage your preferences and API keys.</p>
88
88
89
89
-
<div className="settings-section">
89
89
+
<div className="settings-section layout-settings-section">
90
90
<h2>Layout</h2>
91
91
<div className="layout-options">
92
92
<button
···
331
331
@media (max-width: 600px) {
332
332
.layout-options {
333
333
grid-template-columns: 1fr;
334
334
+
}
335
335
+
}
336
336
+
@media (max-width: 768px) {
337
337
+
.layout-settings-section {
338
338
+
display: none;
334
339
}
335
340
}
336
341
`}</style>