diff --git a/web/src/App.tsx b/web/src/App.tsx index 1727e62..938fc92 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,11 +1,5 @@ import React from "react"; -import { - BrowserRouter, - Routes, - Route, - Navigate, - useSearchParams, -} from "react-router-dom"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { initAuth } from "./store/auth"; import { loadPreferences } from "./store/preferences"; @@ -23,21 +17,12 @@ import { CollectionDetailWrapper, AnnotationDetailWrapper, UserUrlWrapper, - SiteWrapper, + UrlWrapper, } from "./routes/wrappers"; import About from "./views/About"; import AdminModeration from "./views/core/AdminModeration"; import NotFound from "./views/NotFound"; -function UrlRedirect() { - const [searchParams] = useSearchParams(); - const q = searchParams.get("q"); - if (q) { - return ; - } - return ; -} - export default function App() { React.useEffect(() => { initAuth(); @@ -144,14 +129,6 @@ export default function App() { } /> - - - - } - /> - + } /> diff --git a/web/src/components/navigation/MobileNav.tsx b/web/src/components/navigation/MobileNav.tsx index fedccdc..d1df022 100644 --- a/web/src/components/navigation/MobileNav.tsx +++ b/web/src/components/navigation/MobileNav.tsx @@ -205,9 +205,9 @@ export default function MobileNav() { { if (e.key === "Enter" && searchQuery.trim()) { - navigate(`/site/${encodeURIComponent(searchQuery.trim())}`); + navigate(`/url/${encodeURIComponent(searchQuery.trim())}`); } }; diff --git a/web/src/routes/wrappers.tsx b/web/src/routes/wrappers.tsx index d318ae2..d53c59c 100644 --- a/web/src/routes/wrappers.tsx +++ b/web/src/routes/wrappers.tsx @@ -1,12 +1,12 @@ +import { useStore } from "@nanostores/react"; import React from "react"; import { Navigate, useParams } from "react-router-dom"; -import { useStore } from "@nanostores/react"; import { $user } from "../store/auth"; -import Profile from "../views/profile/Profile"; import CollectionDetail from "../views/collections/CollectionDetail"; import AnnotationDetail from "../views/content/AnnotationDetail"; -import UserUrlPage from "../views/content/UserUrl"; -import SitePage from "../views/content/SitePage"; +import UrlPage from "../views/content/UrlPage"; +import UserUrlPage from "../views/content/UserUrlPage"; +import Profile from "../views/profile/Profile"; export function ProfileWrapper() { const { did } = useParams(); @@ -33,6 +33,6 @@ export function UserUrlWrapper() { return ; } -export function SiteWrapper() { - return ; +export function UrlWrapper() { + return ; } diff --git a/web/src/views/content/Url.tsx b/web/src/views/content/Url.tsx deleted file mode 100644 index aa8d9e1..0000000 --- a/web/src/views/content/Url.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useStore } from "@nanostores/react"; -import { $user } from "../../store/auth"; -import { getByTarget, searchActors } from "../../api/client"; -import type { AnnotationItem } from "../../types"; -import Card from "../../components/common/Card"; -import { - Search, - PenTool, - Highlighter, - Loader2, - AlertTriangle, - Copy, - Check, - Clock, - Globe, -} from "lucide-react"; - -import { EmptyState, Tabs, Input, Button } from "../../components/ui"; - -export default function UrlPage() { - const user = useStore($user); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const query = searchParams.get("q"); - - const [annotations, setAnnotations] = useState([]); - const [highlights, setHighlights] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState< - "all" | "annotations" | "highlights" - >("all"); - const [copied, setCopied] = useState(false); - const [recentSearches, setRecentSearches] = useState([]); - const [searched, setSearched] = useState(false); - - useEffect(() => { - const stored = localStorage.getItem("margin-recent-searches"); - if (stored) { - try { - setRecentSearches(JSON.parse(stored).slice(0, 5)); - } catch (e) { - console.warn("Failed to parse recent searches", e); - } - } - }, []); - - const saveRecentSearch = useCallback((q: string) => { - setRecentSearches((prev) => { - const updated = [q, ...prev.filter((s) => s !== q)].slice(0, 5); - localStorage.setItem("margin-recent-searches", JSON.stringify(updated)); - return updated; - }); - }, []); - - useEffect(() => { - const performSearch = async (urlOrHandle: string) => { - if (!urlOrHandle.trim()) return; - - setLoading(true); - setError(null); - setSearched(true); - setAnnotations([]); - setHighlights([]); - - const isProtocol = - urlOrHandle.startsWith("http://") || urlOrHandle.startsWith("https://"); - - if (isProtocol) { - try { - const data = await getByTarget(urlOrHandle); - setAnnotations(data.annotations || []); - setHighlights(data.highlights || []); - saveRecentSearch(urlOrHandle); - } catch (err) { - setError(err instanceof Error ? err.message : "Search failed"); - } finally { - setLoading(false); - } - } else { - try { - const actorRes = await searchActors(urlOrHandle); - if (actorRes?.actors?.length > 0) { - const match = actorRes.actors[0]; - navigate(`/profile/${encodeURIComponent(match.handle)}`, { - replace: true, - }); - return; - } else { - setError( - "User not found. To search for a URL, please include 'http://' or 'https://'.", - ); - setLoading(false); - } - } catch { - setError("Failed to search user."); - setLoading(false); - } - } - }; - - if (query) { - performSearch(query); - } else { - setSearched(false); - setAnnotations([]); - setHighlights([]); - setLoading(false); - } - }, [query, navigate, saveRecentSearch]); - - const myAnnotations = user - ? annotations.filter((a) => (a.author?.did || a.creator?.did) === user.did) - : []; - const myHighlights = user - ? highlights.filter((h) => (h.author?.did || h.creator?.did) === user.did) - : []; - const myItemsCount = myAnnotations.length + myHighlights.length; - - const getShareUrl = () => { - if (!user?.handle || !query) return null; - return `${window.location.origin}/${user.handle}/url/${encodeURIComponent(query)}`; - }; - - const handleCopyShareLink = async () => { - const shareUrl = getShareUrl(); - if (!shareUrl) return; - try { - await navigator.clipboard.writeText(shareUrl); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy link:", err); - } - }; - - const totalItems = annotations.length + highlights.length; - - const renderResults = () => { - if (activeTab === "annotations" && annotations.length === 0) { - return ( - } - title="No annotations" - message="There are no annotations for this URL yet." - /> - ); - } - - if (activeTab === "highlights" && highlights.length === 0) { - return ( - } - title="No highlights" - message="There are no highlights for this URL yet." - /> - ); - } - - return ( -
- {(activeTab === "all" || activeTab === "annotations") && - annotations.map((a) => )} - {(activeTab === "all" || activeTab === "highlights") && - highlights.map((h) => )} -
- ); - }; - - const handleRecentClick = (q: string) => { - navigate(`/url?q=${encodeURIComponent(q)}`); - }; - - return ( -
- {!query && ( -
-
- -
-

- Explore -

-

- Search for any URL in the sidebar to see specific annotations and - highlights. -

- -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const q = formData.get("q") as string; - if (q?.trim()) { - navigate(`/url?q=${encodeURIComponent(q.trim())}`); - } - }} - className="max-w-md mx-auto mb-8 flex gap-2" - > -
- -
- -
- - {recentSearches.length > 0 && ( -
-

- - Recent Searches -

-
- {recentSearches.map((q, i) => ( - - ))} -
-
- )} -
- )} - - {loading && ( -
- -

Searching...

-
- )} - - {error && ( -
- -

{error}

-
- )} - - {searched && !loading && !error && totalItems === 0 && ( - } - title="No results found" - message="We couldn't find any annotations for this URL. Be the first to add one!" - /> - )} - - {searched && !loading && !error && totalItems > 0 && ( -
-
-
-

- {query?.replace(/^https?:\/\//, "")} -

-

- {totalItems} result{totalItems !== 1 ? "s" : ""} found -

-
- - {user && myItemsCount > 0 && ( - - )} -
- -
- - setActiveTab(id as "all" | "annotations" | "highlights") - } - /> -
- - {renderResults()} -
- )} -
- ); -} diff --git a/web/src/views/content/SitePage.tsx b/web/src/views/content/UrlPage.tsx similarity index 86% rename from web/src/views/content/SitePage.tsx rename to web/src/views/content/UrlPage.tsx index d68cbe7..1db57e4 100644 --- a/web/src/views/content/SitePage.tsx +++ b/web/src/views/content/UrlPage.tsx @@ -1,27 +1,26 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { useParams, useNavigate } from "react-router-dom"; import { useStore } from "@nanostores/react"; -import { $user } from "../../store/auth"; -import { getByTarget } from "../../api/client"; -import type { AnnotationItem } from "../../types"; -import Card from "../../components/common/Card"; import { - PenTool, - Highlighter, - Search, AlertTriangle, - Globe, - Copy, Check, + Copy, ExternalLink, + Globe, + Highlighter, Loader2, - Users, + PenTool, + Search, User, + Users, } from "lucide-react"; +import React, { useCallback, useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { getByTarget } from "../../api/client"; +import Card from "../../components/common/Card"; +import { Button, EmptyState, Input, Tabs } from "../../components/ui"; +import { $user } from "../../store/auth"; +import type { AnnotationItem } from "../../types"; -import { Tabs, EmptyState, Input, Button } from "../../components/ui"; - -export default function SitePage() { +export default function UrlPage() { const params = useParams(); const navigate = useNavigate(); const urlPath = params["*"]; @@ -35,7 +34,6 @@ export default function SitePage() { "all" | "annotations" | "highlights" >("all"); const [copied, setCopied] = useState(false); - const [userLinkCopied, setUserLinkCopied] = useState(false); const user = useStore($user); useEffect(() => { @@ -71,17 +69,10 @@ export default function SitePage() { } }, []); - const handleCopyUserLink = useCallback(async () => { + const handleNavigateMyAnnotations = useCallback(async () => { if (!user?.handle || !targetUrl) return; - try { - const url = `${window.location.origin}/${user.handle}/url/${encodeURIComponent(targetUrl)}`; - await navigator.clipboard.writeText(url); - setUserLinkCopied(true); - setTimeout(() => setUserLinkCopied(false), 2000); - } catch (err) { - console.error("Failed to copy user link:", err); - } - }, [user?.handle, targetUrl]); + navigate(`/${user.handle}/url/${encodeURIComponent(targetUrl)}`); + }, [user?.handle, targetUrl, navigate]); const totalItems = annotations.length + highlights.length; @@ -120,7 +111,7 @@ export default function SitePage() { />

- Site Annotations + URL Annotations

Enter a URL to see all public annotations and highlights from the @@ -134,7 +125,7 @@ export default function SitePage() { const q = (formData.get("q") as string)?.trim(); if (q) { const encoded = encodeURIComponent(q); - navigate(`/site/${encoded}`); + navigate(`/url/${encoded}`); } }} className="max-w-md mx-auto flex gap-2" @@ -154,6 +145,19 @@ export default function SitePage() { ); } + const items = [ + ...(activeTab === "all" || activeTab === "annotations" ? annotations : []), + ...(activeTab === "all" || activeTab === "highlights" ? highlights : []), + ]; + + if (activeTab === "all") { + items.sort((a, b) => { + const dateA = new Date(a.createdAt).getTime(); + const dateB = new Date(b.createdAt).getTime(); + return dateB - dateA; + }); + } + return (

@@ -185,12 +189,11 @@ export default function SitePage() {
{user && ( )}
)} diff --git a/web/src/views/content/UserUrl.tsx b/web/src/views/content/UserUrlPage.tsx similarity index 94% rename from web/src/views/content/UserUrl.tsx rename to web/src/views/content/UserUrlPage.tsx index ffaae69..d2d85fe 100644 --- a/web/src/views/content/UserUrl.tsx +++ b/web/src/views/content/UserUrlPage.tsx @@ -1,17 +1,16 @@ -import React, { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { getUserTargetItems } from "../../api/client"; -import type { AnnotationItem, UserProfile } from "../../types"; -import Card from "../../components/common/Card"; +import { clsx } from "clsx"; import { - PenTool, - Highlighter, - Search, AlertTriangle, ExternalLink, + Highlighter, + PenTool, + Search, } from "lucide-react"; -import { clsx } from "clsx"; -import { getAvatarUrl } from "../../api/client"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { getAvatarUrl, getUserTargetItems } from "../../api/client"; +import Card from "../../components/common/Card"; +import type { AnnotationItem, UserProfile } from "../../types"; export default function UserUrlPage() { const params = useParams(); @@ -107,12 +106,26 @@ export default function UserUrlPage() { ); } + const items = [ + ...(activeTab === "all" || activeTab === "annotations" + ? annotations + : []), + ...(activeTab === "all" || activeTab === "highlights" ? highlights : []), + ]; + + if (activeTab === "all") { + items.sort((a, b) => { + const dateA = new Date(a.createdAt).getTime(); + const dateB = new Date(b.createdAt).getTime(); + return dateB - dateA; + }); + } + return (
- {(activeTab === "all" || activeTab === "annotations") && - annotations.map((a) => )} - {(activeTab === "all" || activeTab === "highlights") && - highlights.map((h) => )} + {items.map((item) => ( + + ))}
); }; diff --git a/web/src/views/core/HighlightImporter.tsx b/web/src/views/core/HighlightImporter.tsx index 0e1437f..0f8a599 100644 --- a/web/src/views/core/HighlightImporter.tsx +++ b/web/src/views/core/HighlightImporter.tsx @@ -1,11 +1,12 @@ -import React, { useState, useRef } from "react"; import { - Upload, - Loader2, - CheckCircle2, AlertCircle, + CheckCircle2, Download, + Loader2, + Upload, } from "lucide-react"; +import type React from "react"; +import { useRef, useState } from "react"; import { createHighlight } from "../../api/client"; import type { Selector } from "../../types";