import React, { useState, useCallback, useRef } from "react"; import { AtProtoProvider, TangledRepo } 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 { GrainGallery } from "../lib/components/GrainGallery"; import { CurrentlyPlaying } from "../lib/components/CurrentlyPlaying"; import { LastPlayed } from "../lib/components/LastPlayed"; import { SongHistoryList } from "../lib/components/SongHistoryList"; 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

grain.social Gallery Demo

Instagram-style photo gallery from grain.social

teal.fm Currently Playing

Currently playing track from teal.fm (refreshes every 15s)

teal.fm Last Played

Most recent play from teal.fm feed

teal.fm Song History

Listening history with album art focus

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;