(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix tags

scanash00 fda4506d 4dc85aab

+75 -6
+17
web/src/components/common/Card.tsx
··· 16 16 Flag, 17 17 EyeOff, 18 18 Eye, 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 + )} 528 + 529 + {item.tags && item.tags.length > 0 && ( 530 + <div className="flex flex-wrap gap-2 mt-3"> 531 + {item.tags.map((tag) => ( 532 + <Link 533 + key={tag} 534 + to={`/home?tag=${encodeURIComponent(tag)}`} 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 + onClick={(e) => e.stopPropagation()} 537 + > 538 + <Tag size={10} /> 539 + <span>{tag}</span> 540 + </Link> 541 + ))} 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 + 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 + tag, 31 33 }: { 32 34 type: string; 33 35 motivation?: string; 34 36 emptyMessage: string; 35 37 layout: "list" | "mosaic"; 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 - getFeed({ type, motivation, limit: LIMIT, offset: 0 }) 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 - }, [type, motivation]); 71 + }, [type, motivation, tag]); 69 72 70 73 const loadMore = useCallback(async () => { 71 74 setLoadingMore(true); 72 75 try { 73 - const data = await getFeed({ type, motivation, limit: LIMIT, offset }); 76 + const data = await getFeed({ 77 + type, 78 + motivation, 79 + tag, 80 + limit: LIMIT, 81 + offset, 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 - }, [type, motivation, offset]); 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 + const [searchParams, setSearchParams] = useSearchParams(); 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 + setSearchParams((prev) => { 191 + const newParams = new URLSearchParams(prev); 192 + newParams.delete("tag"); 193 + return newParams; 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 + setSearchParams((prev) => { 203 + const newParams = new URLSearchParams(prev); 204 + newParams.delete("tag"); 205 + return newParams; 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 - <Tabs tabs={tabs} activeTab={activeTab} onChange={handleTabChange} /> 251 + {!tag && ( 252 + <Tabs 253 + tabs={tabs} 254 + activeTab={activeTab} 255 + onChange={handleTabChange} 256 + /> 257 + )} 258 + {tag && ( 259 + <div className="flex items-center justify-between mb-2"> 260 + <h2 className="text-xl font-bold flex items-center gap-2"> 261 + <span className="text-surface-500 font-normal"> 262 + Items with tag: 263 + </span> 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 + #{tag} 266 + </span> 267 + </h2> 268 + <button 269 + onClick={() => { 270 + setSearchParams((prev) => { 271 + const newParams = new URLSearchParams(prev); 272 + newParams.delete("tag"); 273 + return newParams; 274 + }); 275 + }} 276 + className="text-sm text-surface-500 hover:text-surface-900 dark:hover:text-white" 277 + > 278 + Clear filter 279 + </button> 280 + </div> 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 - key={`${activeTab}-${activeFilter || "all"}`} 310 + key={`${activeTab}-${activeFilter || "all"}-${tag || ""}`} 260 311 type={activeTab} 261 312 motivation={activeFilter} 313 + tag={tag} 262 314 emptyMessage={emptyMessage} 263 315 layout={layout} 264 316 />