tangled
alpha
login
or
join now
margin.at
/
margin
86
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
86
fork
atom
overview
issues
4
pulls
1
pipelines
fix tags
scanash.com
1 month ago
fda4506d
4dc85aab
+75
-6
2 changed files
expand all
collapse all
unified
split
web
src
components
common
Card.tsx
views
core
Feed.tsx
+17
web/src/components/common/Card.tsx
···
16
16
Flag,
17
17
EyeOff,
18
18
Eye,
19
19
+
Tag,
19
20
} from "lucide-react";
20
21
import ShareMenu from "../modals/ShareMenu";
21
22
import AddToCollectionModal from "../modals/AddToCollectionModal";
···
523
524
<p className="text-surface-900 dark:text-surface-100 whitespace-pre-wrap leading-relaxed text-[15px]">
524
525
<RichText text={item.body.value} />
525
526
</p>
527
527
+
)}
528
528
+
529
529
+
{item.tags && item.tags.length > 0 && (
530
530
+
<div className="flex flex-wrap gap-2 mt-3">
531
531
+
{item.tags.map((tag) => (
532
532
+
<Link
533
533
+
key={tag}
534
534
+
to={`/home?tag=${encodeURIComponent(tag)}`}
535
535
+
className="inline-flex items-center gap-1 px-2 py-1 rounded-md bg-surface-100 dark:bg-surface-800 text-xs font-medium text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-700 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
536
536
+
onClick={(e) => e.stopPropagation()}
537
537
+
>
538
538
+
<Tag size={10} />
539
539
+
<span>{tag}</span>
540
540
+
</Link>
541
541
+
))}
542
542
+
</div>
526
543
)}
527
544
</div>
528
545
+58
-6
web/src/views/core/Feed.tsx
···
1
1
import React, { useEffect, useState, useCallback } from "react";
2
2
+
import { useSearchParams } from "react-router-dom";
2
3
import { getFeed } from "../../api/client";
3
4
import Card from "../../components/common/Card";
4
5
import {
···
28
29
motivation,
29
30
emptyMessage,
30
31
layout,
32
32
+
tag,
31
33
}: {
32
34
type: string;
33
35
motivation?: string;
34
36
emptyMessage: string;
35
37
layout: "list" | "mosaic";
38
38
+
tag?: string;
36
39
}) {
37
40
const [items, setItems] = useState<AnnotationItem[]>([]);
38
41
const [loading, setLoading] = useState(true);
···
45
48
useEffect(() => {
46
49
let cancelled = false;
47
50
48
48
-
getFeed({ type, motivation, limit: LIMIT, offset: 0 })
51
51
+
getFeed({ type, motivation, tag, limit: LIMIT, offset: 0 })
49
52
.then((data) => {
50
53
if (cancelled) return;
51
54
const fetched = data?.items || [];
···
65
68
return () => {
66
69
cancelled = true;
67
70
};
68
68
-
}, [type, motivation]);
71
71
+
}, [type, motivation, tag]);
69
72
70
73
const loadMore = useCallback(async () => {
71
74
setLoadingMore(true);
72
75
try {
73
73
-
const data = await getFeed({ type, motivation, limit: LIMIT, offset });
76
76
+
const data = await getFeed({
77
77
+
type,
78
78
+
motivation,
79
79
+
tag,
80
80
+
limit: LIMIT,
81
81
+
offset,
82
82
+
});
74
83
const fetched = data?.items || [];
75
84
setItems((prev) => [...prev, ...fetched]);
76
85
setHasMore(fetched.length >= LIMIT);
···
80
89
} finally {
81
90
setLoadingMore(false);
82
91
}
83
83
-
}, [type, motivation, offset]);
92
92
+
}, [type, motivation, tag, offset]);
84
93
85
94
const handleDelete = (uri: string) => {
86
95
setItems((prev) => prev.filter((i) => i.uri !== uri));
···
166
175
showTabs = true,
167
176
emptyMessage = "No items found.",
168
177
}: FeedProps) {
178
178
+
const [searchParams, setSearchParams] = useSearchParams();
179
179
+
const tag = searchParams.get("tag") || undefined;
169
180
const user = useStore($user);
170
181
const layout = useStore($feedLayout);
171
182
const [activeTab, setActiveTab] = useState(initialType);
···
176
187
const handleTabChange = (id: string) => {
177
188
if (id === activeTab) return;
178
189
setActiveTab(id);
190
190
+
setSearchParams((prev) => {
191
191
+
const newParams = new URLSearchParams(prev);
192
192
+
newParams.delete("tag");
193
193
+
return newParams;
194
194
+
});
179
195
window.scrollTo({ top: 0, behavior: "smooth" });
180
196
};
181
197
···
183
199
const next = id === "all" ? undefined : id;
184
200
if (next === activeFilter) return;
185
201
setActiveFilter(next);
202
202
+
setSearchParams((prev) => {
203
203
+
const newParams = new URLSearchParams(prev);
204
204
+
newParams.delete("tag");
205
205
+
return newParams;
206
206
+
});
186
207
window.scrollTo({ top: 0, behavior: "smooth" });
187
208
};
188
209
···
227
248
228
249
{showTabs && (
229
250
<div className="sticky top-0 z-10 bg-surface-50/95 dark:bg-surface-950/95 backdrop-blur-sm pb-3 mb-2 -mx-1 px-1 pt-1 space-y-2">
230
230
-
<Tabs tabs={tabs} activeTab={activeTab} onChange={handleTabChange} />
251
251
+
{!tag && (
252
252
+
<Tabs
253
253
+
tabs={tabs}
254
254
+
activeTab={activeTab}
255
255
+
onChange={handleTabChange}
256
256
+
/>
257
257
+
)}
258
258
+
{tag && (
259
259
+
<div className="flex items-center justify-between mb-2">
260
260
+
<h2 className="text-xl font-bold flex items-center gap-2">
261
261
+
<span className="text-surface-500 font-normal">
262
262
+
Items with tag:
263
263
+
</span>
264
264
+
<span className="bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 px-2 py-0.5 rounded-lg">
265
265
+
#{tag}
266
266
+
</span>
267
267
+
</h2>
268
268
+
<button
269
269
+
onClick={() => {
270
270
+
setSearchParams((prev) => {
271
271
+
const newParams = new URLSearchParams(prev);
272
272
+
newParams.delete("tag");
273
273
+
return newParams;
274
274
+
});
275
275
+
}}
276
276
+
className="text-sm text-surface-500 hover:text-surface-900 dark:hover:text-white"
277
277
+
>
278
278
+
Clear filter
279
279
+
</button>
280
280
+
</div>
281
281
+
)}
231
282
<div className="flex items-center gap-1.5 flex-wrap">
232
283
{filters.map((f) => {
233
284
const isActive =
···
256
307
)}
257
308
258
309
<FeedContent
259
259
-
key={`${activeTab}-${activeFilter || "all"}`}
310
310
+
key={`${activeTab}-${activeFilter || "all"}-${tag || ""}`}
260
311
type={activeTab}
261
312
motivation={activeFilter}
313
313
+
tag={tag}
262
314
emptyMessage={emptyMessage}
263
315
layout={layout}
264
316
/>