import React, { useState, useCallback, useRef } from "react"; import { AtProtoProvider } from "../lib"; import "../lib/styles.css"; import "./App.css"; import { TangledString } from "../lib/components/TangledString"; import { LeafletDocument } from "../lib/components/LeafletDocument"; import { BlueskyProfile } from "../lib/components/BlueskyProfile"; import { BlueskyPost, BLUESKY_POST_COLLECTION, } from "../lib/components/BlueskyPost"; import { BlueskyPostList } from "../lib/components/BlueskyPostList"; import { BlueskyQuotePost } from "../lib/components/BlueskyQuotePost"; import { useDidResolution } from "../lib/hooks/useDidResolution"; import { useLatestRecord } from "../lib/hooks/useLatestRecord"; import type { FeedPostRecord } from "../lib/types/bluesky"; const basicUsageSnippet = `import { AtProtoProvider, BlueskyPost } from 'atproto-ui'; export function App() { return ( ); }`; const prefetchedDataSnippet = `import { BlueskyPost, useLatestRecord } from 'atproto-ui'; import type { FeedPostRecord } from 'atproto-ui'; const LatestPostWithPrefetch: React.FC<{ did: string }> = ({ did }) => { // Fetch once with the hook const { record, rkey, loading } = useLatestRecord( did, 'app.bsky.feed.post' ); if (loading) return Loading…; if (!record || !rkey) return No posts yet.; // Pass prefetched record—BlueskyPost won't re-fetch it return ; };`; const atcuteUsageSnippet = `import { Client, simpleFetchHandler, ok } from '@atcute/client'; import type { AppBskyFeedPost } from '@atcute/bluesky'; import { BlueskyPost } from 'atproto-ui'; // Create atcute client const client = new Client({ handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) }); // Fetch a record const data = await ok( client.get('com.atproto.repo.getRecord', { params: { repo: 'did:plc:ttdrpj45ibqunmfhdsb4zdwq', collection: 'app.bsky.feed.post', rkey: '3m45rq4sjes2h' } }) ); const record = data.value as AppBskyFeedPost.Main; // Pass atcute record directly to component! `; const codeBlockBase: React.CSSProperties = { fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace, monospace', fontSize: 12, whiteSpace: "pre", overflowX: "auto", borderRadius: 10, padding: "12px 14px", lineHeight: 1.6, }; const ThemeSwitcher: React.FC = () => { const [theme, setTheme] = useState<"light" | "dark" | "system">("system"); const toggle = () => { const schemes: ("light" | "dark" | "system")[] = [ "light", "dark", "system", ]; const currentIndex = schemes.indexOf(theme); const nextIndex = (currentIndex + 1) % schemes.length; const nextTheme = schemes[nextIndex]; setTheme(nextTheme); // Update the data-theme attribute on the document element if (nextTheme === "system") { document.documentElement.removeAttribute("data-theme"); } else { document.documentElement.setAttribute("data-theme", nextTheme); } }; return ( ); }; const FullDemo: React.FC = () => { const handleInputRef = useRef(null); const [submitted, setSubmitted] = useState(null); const { did, loading: resolvingDid } = useDidResolution( submitted ?? undefined, ); const onSubmit = useCallback((e) => { e.preventDefault(); const rawValue = handleInputRef.current?.value; const nextValue = rawValue?.trim(); if (!nextValue) return; if (handleInputRef.current) { handleInputRef.current.value = nextValue; } setSubmitted(nextValue); }, []); const showHandle = submitted && !submitted.startsWith("did:") ? submitted : undefined; const panelStyle: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 8, padding: 10, borderRadius: 12, border: `1px solid var(--demo-border)`, }; const gistPanelStyle: React.CSSProperties = { ...panelStyle, padding: 0, border: "none", background: "transparent", backdropFilter: "none", marginTop: 32, }; const leafletPanelStyle: React.CSSProperties = { ...panelStyle, padding: 0, border: "none", background: "transparent", backdropFilter: "none", marginTop: 32, alignItems: "center", }; const primaryGridStyle: React.CSSProperties = { display: "grid", gap: 32, gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))", }; const columnStackStyle: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 32, }; const codeBlockStyle: React.CSSProperties = { ...codeBlockBase, background: `var(--demo-code-bg)`, border: `1px solid var(--demo-code-border)`, color: `var(--demo-text)`, }; const codeTextStyle: React.CSSProperties = { margin: 0, display: "block", fontFamily: codeBlockBase.fontFamily, fontSize: 12, lineHeight: 1.6, whiteSpace: "pre", }; const basicCodeRef = useRef(null); const customCodeRef = useRef(null); // Latest Bluesky post - fetch with record for prefetch demo const { record: latestPostRecord, rkey: latestPostRkey, loading: loadingLatestPost, empty: noPosts, error: latestPostError, } = useLatestRecord(did, BLUESKY_POST_COLLECTION); const quoteSampleDid = "did:plc:ttdrpj45ibqunmfhdsb4zdwq"; const quoteSampleRkey = "3m2prlq6xxc2v"; return (
{!submitted && (

Enter a handle to fetch your profile, latest Bluesky post, a Tangled string, and a Leaflet document.

)} {submitted && resolvingDid && (

Resolving DID…

)} {did && ( <>

Profile

Recent Posts

Latest Post (Prefetched Data)

Using{" "} useLatestRecord {" "} to fetch once, then passing{" "} record {" "} prop—no re-fetch!

{loadingLatestPost && (
Loading latest post…
)} {latestPostError && (
Failed to load latest post.
)} {noPosts && (
No posts found.
)} {!loadingLatestPost && latestPostRkey && latestPostRecord && ( )}

Quote Post Demo

Reply Post Demo

Rich Text Facets Demo

Post with mentions, links, and hashtags

Custom Themed Post

Wrapping a component in a div with custom CSS variables to override the theme!

A Tangled String

A Leaflet Document.

)}

Code Examples

Wrap your app with the provider once and drop the ready-made components wherever you need them.

					
						{basicUsageSnippet}
					
				

Pass prefetched data to components to skip API calls—perfect for SSR or caching.

					
						{prefetchedDataSnippet}
					
				

Use atcute directly to construct records and pass them to components—fully compatible!

					
						{atcuteUsageSnippet}
					
				
); }; const sectionHeaderStyle: React.CSSProperties = { margin: "4px 0", fontSize: 16, color: "var(--demo-text)", }; const loadingBox: React.CSSProperties = { padding: 8 }; const errorBox: React.CSSProperties = { padding: 8, color: "crimson" }; const infoBox: React.CSSProperties = { padding: 8, color: "var(--demo-text-secondary)", }; export const App: React.FC = () => { return (

atproto-ui Demo

A component library for rendering common AT Protocol records for applications such as Bluesky and Tangled.


); }; export default App;