(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
Something went wrong. Try again.
5.3 kB · 190 lines
JSX
at ui-refactor
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191import { useState, useEffect } from "react";import { useAuth } from "../context/AuthContext";import { normalizeAnnotation, normalizeBookmark, likeAnnotation, unlikeAnnotation, getLikeCount, deleteBookmark,} from "../api/client";import { HeartIcon, TrashIcon } from "./Icons";import { Folder, ExternalLink } from "lucide-react";import ShareMenu from "./ShareMenu";import UserMeta from "./UserMeta";
export default function BookmarkCard({ bookmark, onAddToCollection, onDelete,}) { const { user, login } = useAuth(); const raw = bookmark; const data = raw.type === "Bookmark" ? normalizeBookmark(raw) : normalizeAnnotation(raw);
const [likeCount, setLikeCount] = useState(0); const [isLiked, setIsLiked] = useState(false); const [deleting, setDeleting] = useState(false);
const isOwner = user?.did && data.author?.did === user.did; const isSemble = data.uri?.includes("network.cosmik");
let domain = ""; try { if (data.url) domain = new URL(data.url).hostname.replace("www.", ""); } catch { /* ignore */ }
useEffect(() => { let mounted = true; async function fetchData() { try { const likeRes = await getLikeCount(data.uri); if (mounted) { if (likeRes.count !== undefined) setLikeCount(likeRes.count); if (likeRes.liked !== undefined) setIsLiked(likeRes.liked); } } catch { /* ignore */ } } if (data.uri) fetchData(); return () => { mounted = false; }; }, [data.uri]);
const handleLike = async () => { if (!user) { login(); return; } try { if (isLiked) { setIsLiked(false); setLikeCount((prev) => Math.max(0, prev - 1)); await unlikeAnnotation(data.uri); } else { setIsLiked(true); setLikeCount((prev) => prev + 1); const cid = data.cid || ""; if (data.uri && cid) await likeAnnotation(data.uri, cid); } } catch { setIsLiked(!isLiked); setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1)); } };
const handleDelete = async () => { if (onDelete) { onDelete(data.uri); return; } if (!confirm("Delete this bookmark?")) return; try { setDeleting(true); const parts = data.uri.split("/"); const rkey = parts[parts.length - 1]; await deleteBookmark(rkey); window.location.reload(); } catch (err) { alert("Failed to delete: " + err.message); } finally { setDeleting(false); } };
const handleCollect = () => { if (!user) { login(); return; } if (onAddToCollection) onAddToCollection(); };
return ( <article className="card annotation-card bookmark-card"> <header className="annotation-header"> <div className="annotation-header-left"> <UserMeta author={data.author} createdAt={data.createdAt} /> </div> <div className="annotation-header-right"> {isSemble && ( <div className="semble-badge" title="Added using Semble"> <span>via Semble</span> <img src="/semble-logo.svg" alt="Semble" /> </div> )} {((isOwner && !isSemble) || onDelete) && ( <button className="annotation-action action-icon-only" onClick={handleDelete} disabled={deleting} title="Delete" > <TrashIcon size={16} /> </button> )} </div> </header>
<div className="annotation-content"> <a href={data.url} target="_blank" rel="noopener noreferrer" className="bookmark-preview" > <div className="bookmark-preview-content"> <div className="bookmark-preview-site"> <ExternalLink size={12} /> <span>{domain}</span> </div> <h3 className="bookmark-preview-title">{data.title || data.url}</h3> {data.description && ( <p className="bookmark-preview-desc">{data.description}</p> )} </div> </a>
{data.tags?.length > 0 && ( <div className="annotation-tags"> {data.tags.map((tag, i) => ( <span key={i} className="annotation-tag"> #{tag} </span> ))} </div> )} </div>
<footer className="annotation-actions"> <div className="annotation-actions-left"> <button className={`annotation-action ${isLiked ? "liked" : ""}`} onClick={handleLike} > <HeartIcon filled={isLiked} size={16} /> {likeCount > 0 && <span>{likeCount}</span>} </button>
<ShareMenu uri={data.uri} text={data.title || data.description} handle={data.author?.handle} type="Bookmark" url={data.url} />
<button className="annotation-action" onClick={handleCollect}> <Folder size={16} /> <span>Collect</span> </button> </div> </footer> </article> );}