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 * Optional custom renderer for advanced layouts.
34 */
35 renderer?: React.ComponentType<LeafletDocumentRendererInjectedProps>;
36 /**
37 * React node rendered before data begins loading.
38 */
39 fallback?: React.ReactNode;
40 /**
41 * Indicator rendered while data is being fetched from the PDS.
42 */
43 loadingIndicator?: React.ReactNode;
44 /**
45 * Preferred color scheme to forward to the renderer.
46 */
47 colorScheme?: ColorSchemePreference;
48}
49
50/**
51 * Props provided to renderer overrides for Leaflet documents.
52 */
53export type LeafletDocumentRendererInjectedProps = LeafletDocumentRendererProps;
54
55/** NSID for Leaflet document records. */
56export const LEAFLET_DOCUMENT_COLLECTION = "pub.leaflet.document";
57
58/**
59 * Loads a Leaflet document along with its associated publication record and renders it
60 * using the provided or default renderer.
61 *
62 * @param did - DID of the Leaflet publisher.
63 * @param rkey - Record key of the Leaflet document.
64 * @param renderer - Optional renderer override used in place of the default.
65 * @param fallback - Node rendered before loading begins.
66 * @param loadingIndicator - Node rendered while the document or publication records are loading.
67 * @param colorScheme - Preferred color scheme forwarded to the renderer.
68 * @returns A JSX subtree that renders a Leaflet document with contextual metadata.
69 */
70export const LeafletDocument: React.FC<LeafletDocumentProps> = ({
71 did,
72 rkey,
73 renderer,
74 fallback,
75 loadingIndicator,
76 colorScheme,
77}) => {
78 const Comp: React.ComponentType<LeafletDocumentRendererInjectedProps> =
79 renderer ?? ((props) => <LeafletDocumentRenderer {...props} />);
80
81 const Wrapped: React.FC<{
82 record: LeafletDocumentRecord;
83 loading: boolean;
84 error?: Error;
85 }> = (props) => {
86 const publicationUri = useMemo(
87 () => parseAtUri(props.record.publication),
88 [props.record.publication],
89 );
90 const { record: publicationRecord } =
91 useAtProtoRecord<LeafletPublicationRecord>({
92 did: publicationUri?.did,
93 collection:
94 publicationUri?.collection ?? "pub.leaflet.publication",
95 rkey: publicationUri?.rkey ?? "",
96 });
97 const publicationBaseUrl = normalizeLeafletBasePath(
98 publicationRecord?.base_path,
99 );
100 const canonicalUrl = resolveCanonicalUrl(
101 props.record,
102 did,
103 rkey,
104 publicationRecord?.base_path,
105 );
106 return (
107 <Comp
108 {...props}
109 colorScheme={colorScheme}
110 did={did}
111 rkey={rkey}
112 canonicalUrl={canonicalUrl}
113 publicationBaseUrl={publicationBaseUrl}
114 publicationRecord={publicationRecord}
115 />
116 );
117 };
118
119 return (
120 <AtProtoRecord<LeafletDocumentRecord>
121 did={did}
122 collection={LEAFLET_DOCUMENT_COLLECTION}
123 rkey={rkey}
124 renderer={Wrapped}
125 fallback={fallback}
126 loadingIndicator={loadingIndicator}
127 />
128 );
129};
130
131/**
132 * Determines the best canonical URL to expose for a Leaflet document.
133 *
134 * @param record - Leaflet document record under review.
135 * @param did - Publisher DID.
136 * @param rkey - Record key for the document.
137 * @param publicationBasePath - Optional base path configured by the publication.
138 * @returns A URL to use for canonical links.
139 */
140function resolveCanonicalUrl(
141 record: LeafletDocumentRecord,
142 did: string,
143 rkey: string,
144 publicationBasePath?: string,
145): string {
146 const publicationUrl = leafletRkeyUrl(publicationBasePath, rkey);
147 if (publicationUrl) return publicationUrl;
148 const postUri = record.postRef?.uri;
149 if (postUri) {
150 const parsed = parseAtUri(postUri);
151 const href = parsed ? toBlueskyPostUrl(parsed) : undefined;
152 if (href) return href;
153 }
154 return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`;
155}
156
157export default LeafletDocument;