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 (
{showEditModal && (
setShowEditModal(false)}
onUpdate={() => {
window.location.reload();
}}
/>
)}
{isOwnProfile && (
)}
{loading && (
{[1, 2, 3].map((i) => (
))}
)}
{error && (
⚠️
Error loading profile
{error}
)}
{!loading && !error && (
)}
);
}
function AppleIcon({ size = 16 }) {
return (
);
}