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

Configure Feed

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

at frontend-rewrite 182 lines 4.5 kB view raw
1import React, { useEffect, useState } from "react"; 2import { getFeed } from "../../api/client"; 3import Card from "../common/Card"; 4import { Loader2 } from "lucide-react"; 5import { useStore } from "@nanostores/react"; 6import { $user } from "../../store/auth"; 7import type { AnnotationItem } from "../../types"; 8import { Tabs, EmptyState } from "../ui"; 9import LayoutToggle from "../ui/LayoutToggle"; 10import { useStore as useNanoStore } from "@nanostores/react"; 11import { $feedLayout } from "../../store/feedLayout"; 12 13interface MasonryFeedProps { 14 motivation?: string; 15 emptyMessage?: string; 16 showTabs?: boolean; 17 title?: string; 18} 19 20function MasonryContent({ 21 tab, 22 motivation, 23 emptyMessage, 24 userDid, 25 layout, 26}: { 27 tab: string; 28 motivation?: string; 29 emptyMessage: string; 30 userDid?: string; 31 layout: "list" | "mosaic"; 32}) { 33 const [items, setItems] = useState<AnnotationItem[]>([]); 34 const [loading, setLoading] = useState(true); 35 36 useEffect(() => { 37 let cancelled = false; 38 39 const params: { type?: string; motivation?: string; creator?: string } = { 40 motivation, 41 }; 42 43 if (tab === "my" && userDid) { 44 params.creator = userDid; 45 params.type = "my-feed"; 46 } else { 47 params.type = "all"; 48 } 49 50 getFeed(params) 51 .then((data) => { 52 if (cancelled) return; 53 setItems(data?.items || []); 54 setLoading(false); 55 }) 56 .catch((e) => { 57 if (cancelled) return; 58 console.error(e); 59 setItems([]); 60 setLoading(false); 61 }); 62 63 return () => { 64 cancelled = true; 65 }; 66 }, [tab, motivation, userDid]); 67 68 const handleDelete = (uri: string) => { 69 setItems((prev) => prev.filter((i) => i.uri !== uri)); 70 }; 71 72 if (loading) { 73 return ( 74 <div className="flex justify-center py-20"> 75 <Loader2 76 className="animate-spin text-primary-600 dark:text-primary-400" 77 size={32} 78 /> 79 </div> 80 ); 81 } 82 83 if (items.length === 0) { 84 return ( 85 <EmptyState 86 message={ 87 tab === "my" 88 ? emptyMessage 89 : `No ${motivation === "bookmarking" ? "bookmarks" : "highlights"} from the community yet.` 90 } 91 /> 92 ); 93 } 94 95 if (layout === "list") { 96 return ( 97 <div className="space-y-3 animate-fade-in"> 98 {items.map((item) => ( 99 <Card 100 key={item.uri || item.cid} 101 item={item} 102 onDelete={handleDelete} 103 /> 104 ))} 105 </div> 106 ); 107 } 108 109 return ( 110 <div className="columns-1 sm:columns-2 xl:columns-3 2xl:columns-4 gap-4 animate-fade-in"> 111 {items.map((item) => ( 112 <div key={item.uri || item.cid} className="break-inside-avoid mb-4"> 113 <Card item={item} onDelete={handleDelete} /> 114 </div> 115 ))} 116 </div> 117 ); 118} 119 120export default function MasonryFeed({ 121 motivation, 122 emptyMessage = "No items found.", 123 showTabs = false, 124 title, 125}: MasonryFeedProps) { 126 const user = useStore($user); 127 const layout = useNanoStore($feedLayout); 128 const [activeTab, setActiveTab] = useState(user ? "my" : "global"); 129 130 const handleTabChange = (id: string) => { 131 if (id === activeTab) return; 132 setActiveTab(id); 133 window.scrollTo({ top: 0, behavior: "smooth" }); 134 }; 135 136 const tabs = user 137 ? [ 138 { id: "my", label: "My" }, 139 { id: "global", label: "Global" }, 140 ] 141 : [{ id: "global", label: "Global" }]; 142 143 return ( 144 <div className="mx-auto max-w-2xl xl:max-w-none"> 145 {title && ( 146 <h1 className="text-3xl font-display font-bold text-surface-900 dark:text-white mb-6 text-center lg:text-left"> 147 {title} 148 </h1> 149 )} 150 151 {showTabs && ( 152 <div className="sticky top-0 z-10 bg-surface-50/95 dark:bg-surface-950/95 backdrop-blur-sm pb-4 mb-2 -mx-1 px-1 pt-1"> 153 <div className="flex items-center gap-3"> 154 <div className="flex-1"> 155 <Tabs 156 tabs={tabs} 157 activeTab={activeTab} 158 onChange={handleTabChange} 159 /> 160 </div> 161 <LayoutToggle /> 162 </div> 163 </div> 164 )} 165 166 {!showTabs && ( 167 <div className="flex justify-end mb-4"> 168 <LayoutToggle /> 169 </div> 170 )} 171 172 <MasonryContent 173 key={activeTab} 174 tab={activeTab} 175 motivation={motivation} 176 emptyMessage={emptyMessage} 177 userDid={user?.did} 178 layout={layout} 179 /> 180 </div> 181 ); 182}