Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1import { useState, useEffect } from "react";
2import { Link } from "react-router-dom";
3import { useAuth } from "../context/AuthContext";
4import { getUserHighlights, deleteHighlight } from "../api/client";
5import { HighlightIcon } from "../components/Icons";
6import { HighlightCard } from "../components/AnnotationCard";
7
8export default function Highlights() {
9 const { user, isAuthenticated, loading } = useAuth();
10 const [highlights, setHighlights] = useState([]);
11 const [loadingHighlights, setLoadingHighlights] = useState(true);
12 const [error, setError] = useState(null);
13
14 useEffect(() => {
15 async function loadHighlights() {
16 if (!user?.did) return;
17
18 try {
19 setLoadingHighlights(true);
20 const data = await getUserHighlights(user.did);
21 setHighlights(data.items || []);
22 } catch (err) {
23 console.error("Failed to load highlights:", err);
24 setError(err.message);
25 } finally {
26 setLoadingHighlights(false);
27 }
28 }
29
30 if (isAuthenticated && user) {
31 loadHighlights();
32 }
33 }, [isAuthenticated, user]);
34
35 const handleDelete = async (uri) => {
36 if (!confirm("Delete this highlight?")) return;
37
38 try {
39 const parts = uri.split("/");
40 const rkey = parts[parts.length - 1];
41 await deleteHighlight(rkey);
42 setHighlights((prev) => prev.filter((h) => (h.id || h.uri) !== uri));
43 } catch (err) {
44 alert("Failed to delete: " + err.message);
45 }
46 };
47
48 if (loading)
49 return (
50 <div className="page-loading">
51 <div className="spinner"></div>
52 </div>
53 );
54
55 if (!isAuthenticated) {
56 return (
57 <div className="new-page">
58 <div className="card" style={{ textAlign: "center", padding: "48px" }}>
59 <h2>Sign in to view your highlights</h2>
60 <p style={{ color: "var(--text-secondary)", marginTop: "8px" }}>
61 You need to be logged in with your Bluesky account
62 </p>
63 <Link
64 to="/login"
65 className="btn btn-primary"
66 style={{ marginTop: "24px" }}
67 >
68 Sign in with Bluesky
69 </Link>
70 </div>
71 </div>
72 );
73 }
74
75 return (
76 <div className="feed-page">
77 <div className="page-header">
78 <h1 className="page-title">My Highlights</h1>
79 <p className="page-description">
80 Text you've highlighted across the web
81 </p>
82 </div>
83
84 {loadingHighlights ? (
85 <div className="feed-container">
86 <div className="feed">
87 {[1, 2, 3].map((i) => (
88 <div key={i} className="card">
89 <div
90 className="skeleton skeleton-text"
91 style={{ width: "40%" }}
92 ></div>
93 <div className="skeleton skeleton-text"></div>
94 <div
95 className="skeleton skeleton-text"
96 style={{ width: "60%" }}
97 ></div>
98 </div>
99 ))}
100 </div>
101 </div>
102 ) : error ? (
103 <div className="empty-state">
104 <div className="empty-state-icon">⚠️</div>
105 <h3 className="empty-state-title">Error loading highlights</h3>
106 <p className="empty-state-text">{error}</p>
107 </div>
108 ) : highlights.length === 0 ? (
109 <div className="empty-state">
110 <div className="empty-state-icon">
111 <HighlightIcon size={32} />
112 </div>
113 <h3 className="empty-state-title">No highlights yet</h3>
114 <p className="empty-state-text">
115 Highlight text on any page using the browser extension.
116 </p>
117 </div>
118 ) : (
119 <div className="feed-container">
120 <div className="feed">
121 {highlights.map((highlight) => (
122 <HighlightCard
123 key={highlight.id}
124 highlight={highlight}
125 onDelete={handleDelete}
126 />
127 ))}
128 </div>
129 </div>
130 )}
131 </div>
132 );
133}