([]);
- 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.
-
-
-
-
- {recentSearches.length > 0 && (
-
-
-
- Recent Searches
-
-
- {recentSearches.map((q, i) => (
-
- ))}
-
-
- )}
-
- )}
-
- {loading && (
-
- )}
-
- {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 && (
)}
)}
- {(activeTab === "all" || activeTab === "annotations") &&
- annotations.map((a) => )}
- {(activeTab === "all" || activeTab === "highlights") &&
- highlights.map((h) => )}
+ {items.map((item) => (
+
+ ))}
)}
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";