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 if (record !== undefined) {
125 return (
126 <AtProtoRecord<LeafletDocumentRecord>
127 record={record}
128 renderer={Wrapped}
129 fallback={fallback}
130 loadingIndicator={loadingIndicator}
131 />
132 );
133 }
134
135 return (
136 <AtProtoRecord<LeafletDocumentRecord>
137 did={did}
138 collection={LEAFLET_DOCUMENT_COLLECTION}
139 rkey={rkey}
140 renderer={Wrapped}
141 fallback={fallback}
142 loadingIndicator={loadingIndicator}
143 />
144 );
145};
146
147/**
148 * Determines the best canonical URL to expose for a Leaflet document.
149 *
150 * @param record - Leaflet document record under review.
151 * @param did - Publisher DID.
152 * @param rkey - Record key for the document.
153 * @param publicationBasePath - Optional base path configured by the publication.
154 * @returns A URL to use for canonical links.
155 */
156function resolveCanonicalUrl(
157 record: LeafletDocumentRecord,
158 did: string,
159 rkey: string,
160 publicationBasePath?: string,
161): string {
162 const publicationUrl = leafletRkeyUrl(publicationBasePath, rkey);
163 if (publicationUrl) return publicationUrl;
164 const postUri = record.postRef?.uri;
165 if (postUri) {
166 const parsed = parseAtUri(postUri);
167 const href = parsed ? toBlueskyPostUrl(parsed) : undefined;
168 if (href) return href;
169 }
170 return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`;
171}
172
173export default LeafletDocument;