tangled
alpha
login
or
join now
margin.at
/
margin
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
76
fork
atom
overview
issues
2
pulls
pipelines
support user search in url page
scanash.com
2 weeks ago
87cca4da
3be7368d
+198
-11
1 changed file
expand all
collapse all
unified
split
web
src
pages
Url.jsx
+198
-11
web/src/pages/Url.jsx
···
1
1
-
import { useState } from "react";
2
2
-
import { Link } from "react-router-dom";
1
1
+
import { useState, useEffect, useRef } from "react";
2
2
+
import { Link, useNavigate } from "react-router-dom";
3
3
import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
4
4
-
import { getByTarget } from "../api/client";
4
4
+
import { getByTarget, searchActors } from "../api/client";
5
5
import { useAuth } from "../context/AuthContext";
6
6
import { PenIcon, AlertIcon, SearchIcon } from "../components/Icons";
7
7
import { Copy, Check, ExternalLink } from "lucide-react";
8
8
9
9
export default function Url() {
10
10
const { user } = useAuth();
11
11
+
const navigate = useNavigate();
11
12
const [url, setUrl] = useState("");
12
13
const [annotations, setAnnotations] = useState([]);
13
14
const [highlights, setHighlights] = useState([]);
···
17
18
const [activeTab, setActiveTab] = useState("all");
18
19
const [copied, setCopied] = useState(false);
19
20
21
21
+
const [suggestions, setSuggestions] = useState([]);
22
22
+
const [showSuggestions, setShowSuggestions] = useState(false);
23
23
+
const [selectedIndex, setSelectedIndex] = useState(-1);
24
24
+
const inputRef = useRef(null);
25
25
+
const suggestionsRef = useRef(null);
26
26
+
27
27
+
useEffect(() => {
28
28
+
const timer = setTimeout(async () => {
29
29
+
const isUrl = url.includes("http") || url.includes("://");
30
30
+
if (url.length >= 2 && !isUrl) {
31
31
+
try {
32
32
+
const data = await searchActors(url);
33
33
+
setSuggestions(data.actors || []);
34
34
+
setShowSuggestions(true);
35
35
+
} catch {
36
36
+
// ignore
37
37
+
}
38
38
+
} else {
39
39
+
setSuggestions([]);
40
40
+
setShowSuggestions(false);
41
41
+
}
42
42
+
}, 300);
43
43
+
return () => clearTimeout(timer);
44
44
+
}, [url]);
45
45
+
46
46
+
useEffect(() => {
47
47
+
const handleClickOutside = (e) => {
48
48
+
if (
49
49
+
suggestionsRef.current &&
50
50
+
!suggestionsRef.current.contains(e.target) &&
51
51
+
inputRef.current &&
52
52
+
!inputRef.current.contains(e.target)
53
53
+
) {
54
54
+
setShowSuggestions(false);
55
55
+
}
56
56
+
};
57
57
+
document.addEventListener("mousedown", handleClickOutside);
58
58
+
return () => document.removeEventListener("mousedown", handleClickOutside);
59
59
+
}, []);
60
60
+
61
61
+
const handleKeyDown = (e) => {
62
62
+
if (!showSuggestions || suggestions.length === 0) return;
63
63
+
64
64
+
if (e.key === "ArrowDown") {
65
65
+
e.preventDefault();
66
66
+
setSelectedIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
67
67
+
} else if (e.key === "ArrowUp") {
68
68
+
e.preventDefault();
69
69
+
setSelectedIndex((prev) => Math.max(prev - 1, -1));
70
70
+
} else if (e.key === "Enter" && selectedIndex >= 0) {
71
71
+
e.preventDefault();
72
72
+
selectSuggestion(suggestions[selectedIndex]);
73
73
+
} else if (e.key === "Escape") {
74
74
+
setShowSuggestions(false);
75
75
+
}
76
76
+
};
77
77
+
78
78
+
const selectSuggestion = (actor) => {
79
79
+
navigate(`/profile/${encodeURIComponent(actor.handle)}`);
80
80
+
};
81
81
+
20
82
const handleSearch = async (e) => {
21
83
e.preventDefault();
22
84
if (!url.trim()) return;
23
85
86
86
+
setLoading(true);
87
87
+
setError(null);
88
88
+
setSearched(true);
89
89
+
90
90
+
const isProtocol = url.startsWith("http://") || url.startsWith("https://");
91
91
+
if (!isProtocol) {
92
92
+
try {
93
93
+
const actorRes = await searchActors(url);
94
94
+
if (actorRes?.actors?.length > 0) {
95
95
+
const match = actorRes.actors[0];
96
96
+
navigate(`/profile/${encodeURIComponent(match.handle)}`);
97
97
+
return;
98
98
+
}
99
99
+
} catch {
100
100
+
// ignore
101
101
+
}
102
102
+
}
103
103
+
24
104
try {
25
25
-
setLoading(true);
26
26
-
setError(null);
27
27
-
setSearched(true);
28
105
const data = await getByTarget(url);
29
106
setAnnotations(data.annotations || []);
30
107
setHighlights(data.highlights || []);
···
87
164
return (
88
165
<div className="url-page">
89
166
<div className="page-header">
90
90
-
<h1 className="page-title">Browse by URL</h1>
167
167
+
<h1 className="page-title">Explore</h1>
91
168
<p className="page-description">
92
92
-
See annotations and highlights for any webpage
169
169
+
Search for a URL to view its context layer, or find a user by their
170
170
+
handle
93
171
</p>
94
172
</div>
95
173
96
96
-
<form onSubmit={handleSearch} className="url-input-wrapper">
174
174
+
<form
175
175
+
onSubmit={handleSearch}
176
176
+
className="url-input-wrapper"
177
177
+
style={{ position: "relative" }}
178
178
+
>
97
179
<div className="url-input-container">
98
180
<input
99
99
-
type="url"
181
181
+
ref={inputRef}
182
182
+
type="text"
100
183
value={url}
101
184
onChange={(e) => setUrl(e.target.value)}
102
102
-
placeholder="https://example.com/article"
185
185
+
onKeyDown={handleKeyDown}
186
186
+
placeholder="https://... or handle"
103
187
className="url-input"
188
188
+
autoComplete="off"
104
189
required
105
190
/>
106
191
<button type="submit" className="btn btn-primary" disabled={loading}>
107
192
{loading ? "Searching..." : "Search"}
108
193
</button>
109
194
</div>
195
195
+
196
196
+
{showSuggestions && suggestions.length > 0 && (
197
197
+
<div
198
198
+
className="login-suggestions"
199
199
+
ref={suggestionsRef}
200
200
+
style={{
201
201
+
position: "absolute",
202
202
+
top: "100%",
203
203
+
left: 0,
204
204
+
right: 0,
205
205
+
marginTop: "8px",
206
206
+
width: "100%",
207
207
+
zIndex: 50,
208
208
+
background: "var(--bg-primary)",
209
209
+
borderRadius: "12px",
210
210
+
boxShadow: "var(--shadow-lg)",
211
211
+
border: "1px solid var(--border)",
212
212
+
maxHeight: "300px",
213
213
+
overflowY: "auto",
214
214
+
}}
215
215
+
>
216
216
+
{suggestions.map((actor, index) => (
217
217
+
<button
218
218
+
key={actor.did}
219
219
+
type="button"
220
220
+
className={`login-suggestion ${index === selectedIndex ? "selected" : ""}`}
221
221
+
onClick={() => selectSuggestion(actor)}
222
222
+
style={{
223
223
+
width: "100%",
224
224
+
textAlign: "left",
225
225
+
padding: "12px",
226
226
+
display: "flex",
227
227
+
alignItems: "center",
228
228
+
gap: "12px",
229
229
+
border: "none",
230
230
+
background:
231
231
+
index === selectedIndex
232
232
+
? "var(--bg-secondary)"
233
233
+
: "transparent",
234
234
+
cursor: "pointer",
235
235
+
}}
236
236
+
>
237
237
+
<div
238
238
+
className="login-suggestion-avatar"
239
239
+
style={{
240
240
+
width: 32,
241
241
+
height: 32,
242
242
+
borderRadius: "50%",
243
243
+
overflow: "hidden",
244
244
+
background: "var(--bg-tertiary)",
245
245
+
}}
246
246
+
>
247
247
+
{actor.avatar ? (
248
248
+
<img
249
249
+
src={actor.avatar}
250
250
+
alt=""
251
251
+
style={{
252
252
+
width: "100%",
253
253
+
height: "100%",
254
254
+
objectFit: "cover",
255
255
+
}}
256
256
+
/>
257
257
+
) : (
258
258
+
<div
259
259
+
style={{
260
260
+
display: "flex",
261
261
+
alignItems: "center",
262
262
+
justifyContent: "center",
263
263
+
height: "100%",
264
264
+
fontSize: "0.8rem",
265
265
+
}}
266
266
+
>
267
267
+
{(actor.displayName || actor.handle)
268
268
+
.substring(0, 2)
269
269
+
.toUpperCase()}
270
270
+
</div>
271
271
+
)}
272
272
+
</div>
273
273
+
<div
274
274
+
className="login-suggestion-info"
275
275
+
style={{ display: "flex", flexDirection: "column" }}
276
276
+
>
277
277
+
<span
278
278
+
className="login-suggestion-name"
279
279
+
style={{ fontWeight: 600, fontSize: "0.95rem" }}
280
280
+
>
281
281
+
{actor.displayName || actor.handle}
282
282
+
</span>
283
283
+
<span
284
284
+
className="login-suggestion-handle"
285
285
+
style={{
286
286
+
color: "var(--text-secondary)",
287
287
+
fontSize: "0.85rem",
288
288
+
}}
289
289
+
>
290
290
+
@{actor.handle}
291
291
+
</span>
292
292
+
</div>
293
293
+
</button>
294
294
+
))}
295
295
+
</div>
296
296
+
)}
110
297
</form>
111
298
112
299
{error && (