import { useState, useEffect } from "react"; import { useParams, Navigate } from "react-router-dom"; import AnnotationCard, { HighlightCard } from "../components/AnnotationCard"; import BookmarkCard from "../components/BookmarkCard"; import { getLinkIconType, formatUrl } from "../utils/formatting"; import { getUserAnnotations, getUserHighlights, getUserBookmarks, getCollections, getProfile, getAPIKeys, createAPIKey, deleteAPIKey, } from "../api/client"; import { useAuth } from "../context/AuthContext"; import EditProfileModal from "../components/EditProfileModal"; import CollectionIcon from "../components/CollectionIcon"; import CollectionRow from "../components/CollectionRow"; import { PenIcon, HighlightIcon, BookmarkIcon, BlueskyIcon, GithubIcon, LinkedinIcon, TangledIcon, LinkIcon, } from "../components/Icons"; function LinkIconComponent({ url }) { const type = getLinkIconType(url); switch (type) { case "github": return ; case "bluesky": return ; case "linkedin": return ; case "tangled": return ; default: return ; } } function KeyIcon({ size = 16 }) { return ( ); } export default function Profile() { const { handle: routeHandle } = useParams(); const { user, loading: authLoading } = useAuth(); const [activeTab, setActiveTab] = useState("annotations"); const [profile, setProfile] = useState(null); const [annotations, setAnnotations] = useState([]); const [highlights, setHighlights] = useState([]); const [bookmarks, setBookmarks] = useState([]); const [collections, setCollections] = useState([]); const [apiKeys, setApiKeys] = useState([]); const [newKeyName, setNewKeyName] = useState(""); const [newKey, setNewKey] = useState(null); const [keysLoading, setKeysLoading] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showEditModal, setShowEditModal] = useState(false); const handle = routeHandle || user?.did || user?.handle; const isOwnProfile = user && (user.did === handle || user.handle === handle); useEffect(() => { if (!handle) return; async function fetchProfile() { try { setLoading(true); const bskyPromise = fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`, ).then((res) => (res.ok ? res.json() : null)); const marginPromise = getProfile(handle).catch(() => null); const marginData = await marginPromise; let did = handle.startsWith("did:") ? handle : marginData?.did; if (!did) { const bskyData = await bskyPromise; if (bskyData) { did = bskyData.did; setProfile(bskyData); } } else { if (marginData) { setProfile((prev) => ({ ...prev, ...marginData })); } } if (did) { const [annData, hlData, bmData, collData] = await Promise.all([ getUserAnnotations(did), getUserHighlights(did).catch(() => ({ items: [] })), getUserBookmarks(did).catch(() => ({ items: [] })), getCollections(did).catch(() => ({ items: [] })), ]); setAnnotations(annData.items || []); setHighlights(hlData.items || []); setBookmarks(bmData.items || []); setCollections(collData.items || []); const bskyData = await bskyPromise; if (bskyData || marginData) { setProfile((prev) => ({ ...(bskyData || {}), ...prev, ...(marginData || {}), })); } } } catch (err) { console.error(err); setError(err.message); } finally { setLoading(false); } } fetchProfile(); }, [handle]); useEffect(() => { if (isOwnProfile && activeTab === "apikeys") { loadAPIKeys(); } }, [isOwnProfile, activeTab]); const loadAPIKeys = async () => { setKeysLoading(true); try { const data = await getAPIKeys(); setApiKeys(data.keys || []); } catch { setApiKeys([]); } finally { setKeysLoading(false); } }; const handleCreateKey = async () => { if (!newKeyName.trim()) return; try { const data = await createAPIKey(newKeyName.trim()); setNewKey(data.key); setNewKeyName(""); loadAPIKeys(); } catch (err) { alert("Failed to create key: " + err.message); } }; const handleDeleteKey = async (id) => { if (!confirm("Delete this API key? This cannot be undone.")) return; try { await deleteAPIKey(id); loadAPIKeys(); } catch (err) { alert("Failed to delete key: " + err.message); } }; if (authLoading) { return (
{[1, 2, 3].map((i) => (
))}
); } if (!handle) { return ; } const displayName = profile?.displayName || profile?.handle || handle; const displayHandle = profile?.handle || (handle?.startsWith("did:") ? null : handle); const avatarUrl = profile?.did ? `/api/avatar/${encodeURIComponent(profile.did)}` : null; const getInitial = () => { return (displayName || displayHandle || "??") ?.substring(0, 2) .toUpperCase(); }; const totalItems = annotations.length + highlights.length + bookmarks.length + collections.length; const renderContent = () => { if (activeTab === "annotations") { if (annotations.length === 0) { return (

No annotations

This user hasn't posted any annotations.

); } return annotations.map((a) => ( )); } if (activeTab === "highlights") { if (highlights.length === 0) { return (

No highlights

This user hasn't saved any highlights.

); } return highlights.map((h) => ); } if (activeTab === "bookmarks") { if (bookmarks.length === 0) { return (

No bookmarks

This user hasn't bookmarked any pages.

); } return bookmarks.map((b) => ); } if (activeTab === "collections") { if (collections.length === 0) { return (

No collections

This user hasn't created any collections.

); } return (
{collections.map((c) => ( ))}
); } if (activeTab === "apikeys" && isOwnProfile) { return (

Create API Key

Use API keys to create bookmarks from iOS Shortcuts or other tools.

setNewKeyName(e.target.value)} placeholder="Key name (e.g., iOS Shortcut)" className="input" style={{ flex: 1 }} />
{newKey && (

✓ Key created! Copy it now, you won't see it again.

{newKey}
)}
{keysLoading ? (
) : apiKeys.length === 0 ? (

No API keys

Create a key to use with iOS Shortcuts.

) : (

Your API Keys

{apiKeys.map((key) => (
{key.name}
Created {new Date(key.createdAt).toLocaleDateString()} {key.lastUsedAt && ` • Last used ${new Date(key.lastUsedAt).toLocaleDateString()}`}
))}
)}

iOS Shortcut

Save bookmarks from Safari's share sheet.

Get Shortcut
); } }; const bskyProfileUrl = displayHandle ? `https://bsky.app/profile/${displayHandle}` : `https://bsky.app/profile/${handle}`; return (
{avatarUrl ? ( {displayName} ) : ( {getInitial()} )}

{displayName}

{displayHandle && ( @{displayHandle} )}
{totalItems} items {annotations.length} annotations {highlights.length} highlights
{(profile?.bio || profile?.website || profile?.links?.length > 0) && (
{profile.bio &&

{profile.bio}

}
{profile.website && ( {formatUrl(profile.website)} )} {profile.links?.map((link, i) => ( {formatUrl(link)} ))}
)} {isOwnProfile && ( )}
{showEditModal && ( setShowEditModal(false)} onUpdate={() => { window.location.reload(); }} /> )}
{isOwnProfile && ( )}
{loading && (
{[1, 2, 3].map((i) => (
))}
)} {error && (
⚠️

Error loading profile

{error}

)} {!loading && !error && (
{renderContent()}
)}
); } function AppleIcon({ size = 16 }) { return ( ); }