Write on the margins of the internet. Powered by the AT Protocol.
0
fork

Configure Feed

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

fix: remove unused url page

-314
-314
web/src/views/content/Url.tsx
··· 1 - import React, { useState, useEffect, useCallback } from "react"; 2 - import { useNavigate, useSearchParams } from "react-router-dom"; 3 - import { useStore } from "@nanostores/react"; 4 - import { $user } from "../../store/auth"; 5 - import { getByTarget, searchActors } from "../../api/client"; 6 - import type { AnnotationItem } from "../../types"; 7 - import Card from "../../components/common/Card"; 8 - import { 9 - Search, 10 - PenTool, 11 - Highlighter, 12 - Loader2, 13 - AlertTriangle, 14 - Copy, 15 - Check, 16 - Clock, 17 - Globe, 18 - } from "lucide-react"; 19 - 20 - import { EmptyState, Tabs, Input, Button } from "../../components/ui"; 21 - 22 - export default function UrlPage() { 23 - const user = useStore($user); 24 - const navigate = useNavigate(); 25 - const [searchParams] = useSearchParams(); 26 - const query = searchParams.get("q"); 27 - 28 - const [annotations, setAnnotations] = useState<AnnotationItem[]>([]); 29 - const [highlights, setHighlights] = useState<AnnotationItem[]>([]); 30 - const [loading, setLoading] = useState(false); 31 - const [error, setError] = useState<string | null>(null); 32 - const [activeTab, setActiveTab] = useState< 33 - "all" | "annotations" | "highlights" 34 - >("all"); 35 - const [copied, setCopied] = useState(false); 36 - const [recentSearches, setRecentSearches] = useState<string[]>([]); 37 - const [searched, setSearched] = useState(false); 38 - 39 - useEffect(() => { 40 - const stored = localStorage.getItem("margin-recent-searches"); 41 - if (stored) { 42 - try { 43 - setRecentSearches(JSON.parse(stored).slice(0, 5)); 44 - } catch (e) { 45 - console.warn("Failed to parse recent searches", e); 46 - } 47 - } 48 - }, []); 49 - 50 - const saveRecentSearch = useCallback((q: string) => { 51 - setRecentSearches((prev) => { 52 - const updated = [q, ...prev.filter((s) => s !== q)].slice(0, 5); 53 - localStorage.setItem("margin-recent-searches", JSON.stringify(updated)); 54 - return updated; 55 - }); 56 - }, []); 57 - 58 - useEffect(() => { 59 - const performSearch = async (urlOrHandle: string) => { 60 - if (!urlOrHandle.trim()) return; 61 - 62 - setLoading(true); 63 - setError(null); 64 - setSearched(true); 65 - setAnnotations([]); 66 - setHighlights([]); 67 - 68 - const isProtocol = 69 - urlOrHandle.startsWith("http://") || urlOrHandle.startsWith("https://"); 70 - 71 - if (isProtocol) { 72 - try { 73 - const data = await getByTarget(urlOrHandle); 74 - setAnnotations(data.annotations || []); 75 - setHighlights(data.highlights || []); 76 - saveRecentSearch(urlOrHandle); 77 - } catch (err) { 78 - setError(err instanceof Error ? err.message : "Search failed"); 79 - } finally { 80 - setLoading(false); 81 - } 82 - } else { 83 - try { 84 - const actorRes = await searchActors(urlOrHandle); 85 - if (actorRes?.actors?.length > 0) { 86 - const match = actorRes.actors[0]; 87 - navigate(`/profile/${encodeURIComponent(match.handle)}`, { 88 - replace: true, 89 - }); 90 - return; 91 - } else { 92 - setError( 93 - "User not found. To search for a URL, please include 'http://' or 'https://'.", 94 - ); 95 - setLoading(false); 96 - } 97 - } catch { 98 - setError("Failed to search user."); 99 - setLoading(false); 100 - } 101 - } 102 - }; 103 - 104 - if (query) { 105 - performSearch(query); 106 - } else { 107 - setSearched(false); 108 - setAnnotations([]); 109 - setHighlights([]); 110 - setLoading(false); 111 - } 112 - }, [query, navigate, saveRecentSearch]); 113 - 114 - const myAnnotations = user 115 - ? annotations.filter((a) => (a.author?.did || a.creator?.did) === user.did) 116 - : []; 117 - const myHighlights = user 118 - ? highlights.filter((h) => (h.author?.did || h.creator?.did) === user.did) 119 - : []; 120 - const myItemsCount = myAnnotations.length + myHighlights.length; 121 - 122 - const getShareUrl = () => { 123 - if (!user?.handle || !query) return null; 124 - return `${window.location.origin}/${user.handle}/url/${encodeURIComponent(query)}`; 125 - }; 126 - 127 - const handleCopyShareLink = async () => { 128 - const shareUrl = getShareUrl(); 129 - if (!shareUrl) return; 130 - try { 131 - await navigator.clipboard.writeText(shareUrl); 132 - setCopied(true); 133 - setTimeout(() => setCopied(false), 2000); 134 - } catch (err) { 135 - console.error("Failed to copy link:", err); 136 - } 137 - }; 138 - 139 - const totalItems = annotations.length + highlights.length; 140 - 141 - const renderResults = () => { 142 - if (activeTab === "annotations" && annotations.length === 0) { 143 - return ( 144 - <EmptyState 145 - icon={<PenTool size={32} />} 146 - title="No annotations" 147 - message="There are no annotations for this URL yet." 148 - /> 149 - ); 150 - } 151 - 152 - if (activeTab === "highlights" && highlights.length === 0) { 153 - return ( 154 - <EmptyState 155 - icon={<Highlighter size={32} />} 156 - title="No highlights" 157 - message="There are no highlights for this URL yet." 158 - /> 159 - ); 160 - } 161 - 162 - return ( 163 - <div className="space-y-4"> 164 - {(activeTab === "all" || activeTab === "annotations") && 165 - annotations.map((a) => <Card key={a.uri} item={a} />)} 166 - {(activeTab === "all" || activeTab === "highlights") && 167 - highlights.map((h) => <Card key={h.uri} item={h} />)} 168 - </div> 169 - ); 170 - }; 171 - 172 - const handleRecentClick = (q: string) => { 173 - navigate(`/url?q=${encodeURIComponent(q)}`); 174 - }; 175 - 176 - return ( 177 - <div className="max-w-2xl mx-auto pb-20 animate-fade-in"> 178 - {!query && ( 179 - <div className="text-center py-10"> 180 - <div className="w-16 h-16 bg-primary-50 dark:bg-primary-900/20 rounded-2xl flex items-center justify-center mx-auto mb-6 rotate-3"> 181 - <Search 182 - size={32} 183 - className="text-primary-600 dark:text-primary-400" 184 - /> 185 - </div> 186 - <h1 className="text-3xl font-display font-bold text-surface-900 dark:text-white mb-3"> 187 - Explore 188 - </h1> 189 - <p className="text-surface-500 dark:text-surface-400 max-w-md mx-auto mb-8"> 190 - Search for any URL in the sidebar to see specific annotations and 191 - highlights. 192 - </p> 193 - 194 - <form 195 - onSubmit={(e) => { 196 - e.preventDefault(); 197 - const formData = new FormData(e.currentTarget); 198 - const q = formData.get("q") as string; 199 - if (q?.trim()) { 200 - navigate(`/url?q=${encodeURIComponent(q.trim())}`); 201 - } 202 - }} 203 - className="max-w-md mx-auto mb-8 flex gap-2" 204 - > 205 - <div className="flex-1"> 206 - <Input 207 - name="q" 208 - placeholder="https://example.com" 209 - className="w-full bg-surface-50 dark:bg-surface-800" 210 - autoFocus 211 - /> 212 - </div> 213 - <Button type="submit">Search</Button> 214 - </form> 215 - 216 - {recentSearches.length > 0 && ( 217 - <div className="text-left max-w-lg mx-auto bg-surface-50 dark:bg-surface-800/50 rounded-2xl p-6 border border-surface-100 dark:border-surface-800"> 218 - <h3 className="text-sm font-bold text-surface-900 dark:text-white mb-4 flex items-center gap-2"> 219 - <Clock size={16} className="text-primary-500" /> 220 - Recent Searches 221 - </h3> 222 - <div className="flex flex-wrap gap-2"> 223 - {recentSearches.map((q, i) => ( 224 - <button 225 - key={i} 226 - onClick={() => handleRecentClick(q)} 227 - className="px-3 py-1.5 bg-white dark:bg-surface-700 hover:bg-surface-50 dark:hover:bg-surface-600 rounded-lg text-sm text-surface-700 dark:text-surface-200 transition-colors shadow-sm ring-1 ring-black/5 dark:ring-white/5 flex items-center gap-2" 228 - > 229 - <Globe size={12} className="opacity-50" /> 230 - <span className="truncate max-w-[200px]"> 231 - {q.replace(/^https?:\/\//, "")} 232 - </span> 233 - </button> 234 - ))} 235 - </div> 236 - </div> 237 - )} 238 - </div> 239 - )} 240 - 241 - {loading && ( 242 - <div className="flex flex-col items-center justify-center py-20"> 243 - <Loader2 244 - className="animate-spin text-primary-600 dark:text-primary-400 mb-4" 245 - size={32} 246 - /> 247 - <p className="text-surface-500 dark:text-surface-400">Searching...</p> 248 - </div> 249 - )} 250 - 251 - {error && ( 252 - <div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-xl flex items-start gap-3 border border-red-100 dark:border-red-900/30 mb-6"> 253 - <AlertTriangle className="shrink-0 mt-0.5" size={18} /> 254 - <p>{error}</p> 255 - </div> 256 - )} 257 - 258 - {searched && !loading && !error && totalItems === 0 && ( 259 - <EmptyState 260 - icon={<Search size={48} />} 261 - title="No results found" 262 - message="We couldn't find any annotations for this URL. Be the first to add one!" 263 - /> 264 - )} 265 - 266 - {searched && !loading && !error && totalItems > 0 && ( 267 - <div> 268 - <div className="flex items-center justify-between gap-4 mb-6"> 269 - <div> 270 - <h1 className="text-2xl font-bold text-surface-900 dark:text-white truncate max-w-md"> 271 - {query?.replace(/^https?:\/\//, "")} 272 - </h1> 273 - <p className="text-surface-500 dark:text-surface-400 text-sm"> 274 - {totalItems} result{totalItems !== 1 ? "s" : ""} found 275 - </p> 276 - </div> 277 - 278 - {user && myItemsCount > 0 && ( 279 - <button 280 - onClick={handleCopyShareLink} 281 - className="flex items-center gap-1.5 px-3 py-1.5 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-900 dark:text-white text-sm font-medium rounded-lg transition-colors" 282 - > 283 - {copied ? <Check size={14} /> : <Copy size={14} />} 284 - {copied ? "Copied" : "Share your thoughts on this URL"} 285 - </button> 286 - )} 287 - </div> 288 - 289 - <div className="mb-6"> 290 - <Tabs 291 - tabs={[ 292 - { id: "all", label: `All (${totalItems})` }, 293 - { 294 - id: "annotations", 295 - label: `Annotations (${annotations.length})`, 296 - }, 297 - { 298 - id: "highlights", 299 - label: `Highlights (${highlights.length})`, 300 - }, 301 - ]} 302 - activeTab={activeTab} 303 - onChange={(id: string) => 304 - setActiveTab(id as "all" | "annotations" | "highlights") 305 - } 306 - /> 307 - </div> 308 - 309 - {renderResults()} 310 - </div> 311 - )} 312 - </div> 313 - ); 314 - }