Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 166 lines 5.9 kB view raw
1import { useStore } from "@nanostores/react"; 2import { clsx } from "clsx"; 3import { Bookmark, Highlighter, MessageSquareText } from "lucide-react"; 4import { useState } from "react"; 5import { useSearchParams } from "react-router-dom"; 6import FeedItems from "../../components/feed/FeedItems"; 7import { Button, Tabs } from "../../components/ui"; 8import LayoutToggle from "../../components/ui/LayoutToggle"; 9import { $user } from "../../store/auth"; 10import { $feedLayout } from "../../store/feedLayout"; 11 12interface FeedProps { 13 initialType?: string; 14 motivation?: string; 15 showTabs?: boolean; 16 emptyMessage?: string; 17} 18 19export default function Feed({ 20 initialType = "all", 21 motivation, 22 showTabs = true, 23 emptyMessage = "No items found.", 24}: FeedProps) { 25 const [searchParams, setSearchParams] = useSearchParams(); 26 const tag = searchParams.get("tag") || undefined; 27 const user = useStore($user); 28 const layout = useStore($feedLayout); 29 const [activeTab, setActiveTab] = useState(initialType); 30 const [activeFilter, setActiveFilter] = useState<string | undefined>( 31 motivation, 32 ); 33 34 const handleTabChange = (id: string) => { 35 if (id === activeTab) return; 36 setActiveTab(id); 37 setSearchParams((prev) => { 38 const newParams = new URLSearchParams(prev); 39 newParams.delete("tag"); 40 return newParams; 41 }); 42 window.scrollTo({ top: 0, behavior: "smooth" }); 43 }; 44 45 const handleFilterChange = (id: string) => { 46 const next = id === "all" ? undefined : id; 47 if (next === activeFilter) return; 48 setActiveFilter(next); 49 setSearchParams((prev) => { 50 const newParams = new URLSearchParams(prev); 51 newParams.delete("tag"); 52 return newParams; 53 }); 54 window.scrollTo({ top: 0, behavior: "smooth" }); 55 }; 56 57 const tabs = [ 58 { id: "all", label: "Recent" }, 59 { id: "popular", label: "Popular" }, 60 { id: "shelved", label: "Shelved" }, 61 { id: "margin", label: "Margin" }, 62 { id: "semble", label: "Semble" }, 63 ]; 64 65 const filters = [ 66 { id: "all", label: "All", icon: null }, 67 { id: "commenting", label: "Annotations", icon: MessageSquareText }, 68 { id: "highlighting", label: "Highlights", icon: Highlighter }, 69 { id: "bookmarking", label: "Bookmarks", icon: Bookmark }, 70 ]; 71 72 return ( 73 <div className="mx-auto max-w-2xl xl:max-w-none"> 74 {!user && ( 75 <div className="text-center py-10 px-6 mb-4 animate-fade-in"> 76 <h1 className="text-2xl font-display font-bold mb-2 tracking-tight text-surface-900 dark:text-white"> 77 Welcome to Margin 78 </h1> 79 <p className="text-surface-500 dark:text-surface-400 mb-4 max-w-md mx-auto"> 80 Annotate, highlight, and bookmark anything on the web. 81 </p> 82 <div className="flex gap-3 justify-center"> 83 <Button onClick={() => (window.location.href = "/login")}> 84 Get Started 85 </Button> 86 <Button 87 variant="secondary" 88 onClick={() => window.open("/about", "_blank")} 89 > 90 Learn More 91 </Button> 92 </div> 93 </div> 94 )} 95 96 {showTabs && ( 97 <div className="sticky top-0 z-10 bg-white/95 dark:bg-surface-800/95 backdrop-blur-sm pb-3 mb-2 -mx-1 px-1 pt-1 space-y-2"> 98 {!tag && ( 99 <Tabs 100 tabs={tabs} 101 activeTab={activeTab} 102 onChange={handleTabChange} 103 /> 104 )} 105 {tag && ( 106 <div className="flex items-center justify-between mb-2"> 107 <h2 className="text-xl font-bold flex items-center gap-2"> 108 <span className="text-surface-500 font-normal"> 109 Items with tag: 110 </span> 111 <span className="bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 px-2 py-0.5 rounded-lg"> 112 #{tag} 113 </span> 114 </h2> 115 <button 116 onClick={() => { 117 setSearchParams((prev) => { 118 const newParams = new URLSearchParams(prev); 119 newParams.delete("tag"); 120 return newParams; 121 }); 122 }} 123 className="text-sm text-surface-500 hover:text-surface-900 dark:hover:text-white" 124 > 125 Clear filter 126 </button> 127 </div> 128 )} 129 <div className="flex items-center gap-1.5 flex-wrap"> 130 {filters.map((f) => { 131 const isActive = 132 f.id === "all" ? !activeFilter : activeFilter === f.id; 133 return ( 134 <button 135 key={f.id} 136 onClick={() => handleFilterChange(f.id)} 137 className={clsx( 138 "inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-full border transition-all", 139 isActive 140 ? "bg-primary-600 dark:bg-primary-500 text-white border-transparent shadow-sm" 141 : "bg-white dark:bg-surface-900 text-surface-500 dark:text-surface-400 border-surface-200 dark:border-surface-700 hover:border-primary-300 dark:hover:border-primary-700 hover:text-primary-600 dark:hover:text-primary-400", 142 )} 143 > 144 {f.icon && <f.icon size={12} />} 145 {f.label} 146 </button> 147 ); 148 })} 149 <div className="ml-auto"> 150 <LayoutToggle className="hidden sm:inline-flex" /> 151 </div> 152 </div> 153 </div> 154 )} 155 156 <FeedItems 157 key={`${activeTab}-${activeFilter || "all"}-${tag || ""}`} 158 type={activeTab} 159 motivation={activeFilter} 160 emptyMessage={emptyMessage} 161 layout={layout} 162 tag={tag} 163 /> 164 </div> 165 ); 166}