A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React, { useMemo } from "react"; 2import { AtProtoRecord } from "../core/AtProtoRecord"; 3import { 4 LeafletDocumentRenderer, 5 type LeafletDocumentRendererProps, 6} from "../renderers/LeafletDocumentRenderer"; 7import type { 8 LeafletDocumentRecord, 9 LeafletPublicationRecord, 10} from "../types/leaflet"; 11import type { ColorSchemePreference } from "../hooks/useColorScheme"; 12import { 13 parseAtUri, 14 toBlueskyPostUrl, 15 leafletRkeyUrl, 16 normalizeLeafletBasePath, 17} from "../utils/at-uri"; 18import { useAtProtoRecord } from "../hooks/useAtProtoRecord"; 19 20/** 21 * Props for rendering a Leaflet document record. 22 */ 23export interface LeafletDocumentProps { 24 /** 25 * DID of the Leaflet publisher. 26 */ 27 did: string; 28 /** 29 * Record key of the document within the Leaflet collection. 30 */ 31 rkey: string; 32 /** 33 * Prefetched Leaflet document record. When provided, skips fetching from the network. 34 */ 35 record?: LeafletDocumentRecord; 36 /** 37 * Optional custom renderer for advanced layouts. 38 */ 39 renderer?: React.ComponentType<LeafletDocumentRendererInjectedProps>; 40 /** 41 * React node rendered before data begins loading. 42 */ 43 fallback?: React.ReactNode; 44 /** 45 * Indicator rendered while data is being fetched from the PDS. 46 */ 47 loadingIndicator?: React.ReactNode; 48 /** 49 * Preferred color scheme to forward to the renderer. 50 */ 51 colorScheme?: ColorSchemePreference; 52} 53 54/** 55 * Props provided to renderer overrides for Leaflet documents. 56 */ 57export type LeafletDocumentRendererInjectedProps = LeafletDocumentRendererProps; 58 59/** NSID for Leaflet document records. */ 60export const LEAFLET_DOCUMENT_COLLECTION = "pub.leaflet.document"; 61 62/** 63 * Loads a Leaflet document along with its associated publication record and renders it 64 * using the provided or default renderer. 65 * 66 * @param did - DID of the Leaflet publisher. 67 * @param rkey - Record key of the Leaflet document. 68 * @param renderer - Optional renderer override used in place of the default. 69 * @param fallback - Node rendered before loading begins. 70 * @param loadingIndicator - Node rendered while the document or publication records are loading. 71 * @param colorScheme - Preferred color scheme forwarded to the renderer. 72 * @returns A JSX subtree that renders a Leaflet document with contextual metadata. 73 */ 74export const LeafletDocument: React.FC<LeafletDocumentProps> = ({ 75 did, 76 rkey, 77 record, 78 renderer, 79 fallback, 80 loadingIndicator, 81 colorScheme, 82}) => { 83 const Comp: React.ComponentType<LeafletDocumentRendererInjectedProps> = 84 renderer ?? ((props) => <LeafletDocumentRenderer {...props} />); 85 86 const Wrapped: React.FC<{ 87 record: LeafletDocumentRecord; 88 loading: boolean; 89 error?: Error; 90 }> = (props) => { 91 const publicationUri = useMemo( 92 () => parseAtUri(props.record.publication), 93 [props.record.publication], 94 ); 95 const { record: publicationRecord } = 96 useAtProtoRecord<LeafletPublicationRecord>({ 97 did: publicationUri?.did, 98 collection: 99 publicationUri?.collection ?? "pub.leaflet.publication", 100 rkey: publicationUri?.rkey ?? "", 101 }); 102 const publicationBaseUrl = normalizeLeafletBasePath( 103 publicationRecord?.base_path, 104 ); 105 const canonicalUrl = resolveCanonicalUrl( 106 props.record, 107 did, 108 rkey, 109 publicationRecord?.base_path, 110 ); 111 return ( 112 <Comp 113 {...props} 114 colorScheme={colorScheme} 115 did={did} 116 rkey={rkey} 117 canonicalUrl={canonicalUrl} 118 publicationBaseUrl={publicationBaseUrl} 119 publicationRecord={publicationRecord} 120 /> 121 ); 122 }; 123 124 // When record is provided, pass it directly to skip fetching 125 if (record) { 126 return ( 127 <AtProtoRecord<LeafletDocumentRecord> 128 record={record} 129 renderer={Wrapped} 130 fallback={fallback} 131 loadingIndicator={loadingIndicator} 132 /> 133 ); 134 } 135 136 // Otherwise fetch the record using did, collection, and rkey 137 return ( 138 <AtProtoRecord<LeafletDocumentRecord> 139 did={did} 140 collection={LEAFLET_DOCUMENT_COLLECTION} 141 rkey={rkey} 142 renderer={Wrapped} 143 fallback={fallback} 144 loadingIndicator={loadingIndicator} 145 /> 146 ); 147}; 148 149/** 150 * Determines the best canonical URL to expose for a Leaflet document. 151 * 152 * @param record - Leaflet document record under review. 153 * @param did - Publisher DID. 154 * @param rkey - Record key for the document. 155 * @param publicationBasePath - Optional base path configured by the publication. 156 * @returns A URL to use for canonical links. 157 */ 158function resolveCanonicalUrl( 159 record: LeafletDocumentRecord, 160 did: string, 161 rkey: string, 162 publicationBasePath?: string, 163): string { 164 const publicationUrl = leafletRkeyUrl(publicationBasePath, rkey); 165 if (publicationUrl) return publicationUrl; 166 const postUri = record.postRef?.uri; 167 if (postUri) { 168 const parsed = parseAtUri(postUri); 169 const href = parsed ? toBlueskyPostUrl(parsed) : undefined; 170 if (href) return href; 171 } 172 return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`; 173} 174 175export default LeafletDocument;