Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

fix tags

+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 />