A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
40
fork

Configure Feed

Select the types of activity you want to include in your feed.

version bump, clean up demo a bit

+24 -161
+1 -1
package.json
··· 1 1 { 2 2 "name": "atproto-ui", 3 - "version": "0.3.1-1", 3 + "version": "0.4.0", 4 4 "type": "module", 5 5 "description": "React components and hooks for rendering AT Protocol records.", 6 6 "main": "./lib-dist/index.js",
+23 -160
src/App.tsx
··· 6 6 useRef, 7 7 } from "react"; 8 8 import { AtProtoProvider } from "../lib/providers/AtProtoProvider"; 9 - import { AtProtoRecord } from "../lib/core/AtProtoRecord"; 9 + 10 10 import { TangledString } from "../lib/components/TangledString"; 11 11 import { LeafletDocument } from "../lib/components/LeafletDocument"; 12 12 import { BlueskyProfile } from "../lib/components/BlueskyProfile"; ··· 37 37 ); 38 38 }`; 39 39 40 - const customComponentSnippet = `import { useLatestRecord, useColorScheme, AtProtoRecord } from 'atproto-ui'; 40 + const prefetchedDataSnippet = `import { BlueskyPost, useLatestRecord } from 'atproto-ui'; 41 41 import type { FeedPostRecord } from 'atproto-ui'; 42 42 43 - const LatestPostSummary: React.FC<{ did: string }> = ({ did }) => { 44 - const scheme = useColorScheme('system'); 45 - const { rkey, loading, error } = useLatestRecord<FeedPostRecord>(did, 'app.bsky.feed.post'); 43 + const LatestPostWithPrefetch: React.FC<{ did: string }> = ({ did }) => { 44 + // Fetch once with the hook 45 + const { record, rkey, loading } = useLatestRecord<FeedPostRecord>( 46 + did, 47 + 'app.bsky.feed.post' 48 + ); 46 49 47 50 if (loading) return <span>Loading…</span>; 48 - if (error || !rkey) return <span>No post yet.</span>; 51 + if (!record || !rkey) return <span>No posts yet.</span>; 49 52 50 - return ( 51 - <AtProtoRecord<FeedPostRecord> 52 - did={did} 53 - collection="app.bsky.feed.post" 54 - rkey={rkey} 55 - renderer={({ record }) => ( 56 - <article data-color-scheme={scheme}> 57 - <strong>{record?.text ?? 'Empty post'}</strong> 58 - </article> 59 - )} 60 - /> 61 - ); 53 + // Pass prefetched record—BlueskyPost won't re-fetch it 54 + return <BlueskyPost did={did} rkey={rkey} record={record} />; 62 55 };`; 63 56 64 57 const codeBlockBase: React.CSSProperties = { ··· 219 212 const basicCodeRef = useRef<HTMLElement | null>(null); 220 213 const customCodeRef = useRef<HTMLElement | null>(null); 221 214 222 - // Latest Bluesky post 215 + // Latest Bluesky post - fetch with record for prefetch demo 223 216 const { 217 + record: latestPostRecord, 224 218 rkey: latestPostRkey, 225 219 loading: loadingLatestPost, 226 220 empty: noPosts, 227 221 error: latestPostError, 228 - } = useLatestRecord<unknown>(did, BLUESKY_POST_COLLECTION); 222 + } = useLatestRecord<FeedPostRecord>(did, BLUESKY_POST_COLLECTION); 229 223 230 224 const quoteSampleDid = "did:plc:ttdrpj45ibqunmfhdsb4zdwq"; 231 225 const quoteSampleRkey = "3m2prlq6xxc2v"; ··· 323 317 <div style={columnStackStyle}> 324 318 <section style={panelStyle}> 325 319 <h3 style={sectionHeaderStyle}> 326 - Latest Bluesky Post 320 + Latest Post (Prefetched Data) 327 321 </h3> 322 + <p style={{ fontSize: 12, color: mutedTextColor, margin: "0 0 8px" }}> 323 + Using <code style={{ background: scheme === "dark" ? "#1e293b" : "#e2e8f0", padding: "2px 4px", borderRadius: 3 }}>useLatestRecord</code> to fetch once, then passing <code style={{ background: scheme === "dark" ? "#1e293b" : "#e2e8f0", padding: "2px 4px", borderRadius: 3 }}>record</code> prop—no re-fetch! 324 + </p> 328 325 {loadingLatestPost && ( 329 326 <div style={loadingBox}> 330 327 Loading latest post… ··· 345 342 No posts found. 346 343 </div> 347 344 )} 348 - {!loadingLatestPost && latestPostRkey && ( 345 + {!loadingLatestPost && latestPostRkey && latestPostRecord && ( 349 346 <BlueskyPost 350 347 did={did} 351 348 rkey={latestPostRkey} 349 + record={latestPostRecord} 352 350 colorScheme={colorSchemePreference} 353 351 /> 354 352 )} ··· 392 390 </> 393 391 )} 394 392 <section style={{ ...panelStyle, marginTop: 32 }}> 395 - <h3 style={sectionHeaderStyle}>Build your own component</h3> 393 + <h3 style={sectionHeaderStyle}>Code Examples</h3> 396 394 <p style={{ color: mutedTextColor, margin: "4px 0 8px" }}> 397 395 Wrap your app with the provider once and drop the ready-made 398 396 components wherever you need them. ··· 407 405 </code> 408 406 </pre> 409 407 <p style={{ color: mutedTextColor, margin: "16px 0 8px" }}> 410 - Need to make your own component? Compose your own renderer 411 - with the hooks and utilities that ship with the library. 408 + Pass prefetched data to components to skip API calls—perfect for SSR or caching. 412 409 </p> 413 410 <pre style={codeBlockStyle}> 414 411 <code ··· 416 413 className="language-tsx" 417 414 style={codeTextStyle} 418 415 > 419 - {customComponentSnippet} 416 + {prefetchedDataSnippet} 420 417 </code> 421 418 </pre> 422 - {did && ( 423 - <div 424 - style={{ 425 - marginTop: 16, 426 - display: "flex", 427 - flexDirection: "column", 428 - gap: 12, 429 - }} 430 - > 431 - <p style={{ color: mutedTextColor, margin: 0 }}> 432 - Live example with your handle: 433 - </p> 434 - <LatestPostSummary 435 - did={did} 436 - handle={showHandle} 437 - colorScheme={colorSchemePreference} 438 - /> 439 - </div> 440 - )} 441 419 </section> 442 420 </div> 443 421 ); 444 422 }; 445 423 446 - const LatestPostSummary: React.FC<{ 447 - did: string; 448 - handle?: string; 449 - colorScheme: ColorSchemePreference; 450 - }> = ({ did, colorScheme }) => { 451 - const { record, rkey, loading, error } = useLatestRecord<FeedPostRecord>( 452 - did, 453 - BLUESKY_POST_COLLECTION, 454 - ); 455 - const scheme = useColorScheme(colorScheme); 456 - const palette = 457 - scheme === "dark" 458 - ? latestSummaryPalette.dark 459 - : latestSummaryPalette.light; 460 424 461 - if (loading) return <div style={palette.muted}>Loading summary…</div>; 462 - if (error) 463 - return <div style={palette.error}>Failed to load the latest post.</div>; 464 - if (!rkey) return <div style={palette.muted}>No posts published yet.</div>; 465 - 466 - const atProtoProps = record 467 - ? { record } 468 - : { did, collection: "app.bsky.feed.post", rkey }; 469 - 470 - return ( 471 - <AtProtoRecord<FeedPostRecord> 472 - {...atProtoProps} 473 - renderer={({ record: resolvedRecord }) => ( 474 - <article data-color-scheme={scheme}> 475 - <strong>{resolvedRecord?.text ?? "Empty post"}</strong> 476 - </article> 477 - )} 478 - /> 479 - ); 480 - }; 481 425 482 426 const sectionHeaderStyle: React.CSSProperties = { 483 427 margin: "4px 0", ··· 486 430 const loadingBox: React.CSSProperties = { padding: 8 }; 487 431 const errorBox: React.CSSProperties = { padding: 8, color: "crimson" }; 488 432 const infoBox: React.CSSProperties = { padding: 8, color: "#555" }; 489 - 490 - const latestSummaryPalette = { 491 - light: { 492 - card: { 493 - border: "1px solid #e2e8f0", 494 - background: "#ffffff", 495 - borderRadius: 12, 496 - padding: 12, 497 - display: "flex", 498 - flexDirection: "column", 499 - gap: 8, 500 - } satisfies React.CSSProperties, 501 - header: { 502 - display: "flex", 503 - alignItems: "baseline", 504 - justifyContent: "space-between", 505 - gap: 12, 506 - color: "#0f172a", 507 - } satisfies React.CSSProperties, 508 - time: { 509 - fontSize: 12, 510 - color: "#64748b", 511 - } satisfies React.CSSProperties, 512 - text: { 513 - margin: 0, 514 - color: "#1f2937", 515 - whiteSpace: "pre-wrap", 516 - } satisfies React.CSSProperties, 517 - link: { 518 - color: "#2563eb", 519 - fontWeight: 600, 520 - fontSize: 12, 521 - textDecoration: "none", 522 - } satisfies React.CSSProperties, 523 - muted: { 524 - color: "#64748b", 525 - } satisfies React.CSSProperties, 526 - error: { 527 - color: "crimson", 528 - } satisfies React.CSSProperties, 529 - }, 530 - dark: { 531 - card: { 532 - border: "1px solid #1e293b", 533 - background: "#0f172a", 534 - borderRadius: 12, 535 - padding: 12, 536 - display: "flex", 537 - flexDirection: "column", 538 - gap: 8, 539 - } satisfies React.CSSProperties, 540 - header: { 541 - display: "flex", 542 - alignItems: "baseline", 543 - justifyContent: "space-between", 544 - gap: 12, 545 - color: "#e2e8f0", 546 - } satisfies React.CSSProperties, 547 - time: { 548 - fontSize: 12, 549 - color: "#cbd5f5", 550 - } satisfies React.CSSProperties, 551 - text: { 552 - margin: 0, 553 - color: "#e2e8f0", 554 - whiteSpace: "pre-wrap", 555 - } satisfies React.CSSProperties, 556 - link: { 557 - color: "#38bdf8", 558 - fontWeight: 600, 559 - fontSize: 12, 560 - textDecoration: "none", 561 - } satisfies React.CSSProperties, 562 - muted: { 563 - color: "#94a3b8", 564 - } satisfies React.CSSProperties, 565 - error: { 566 - color: "#f472b6", 567 - } satisfies React.CSSProperties, 568 - }, 569 - } as const; 570 433 571 434 export const App: React.FC = () => { 572 435 return (