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 { LeafletDocumentRenderer, type LeafletDocumentRendererProps } from '../renderers/LeafletDocumentRenderer';
4import type { LeafletDocumentRecord, LeafletPublicationRecord } from '../types/leaflet';
5import type { ColorSchemePreference } from '../hooks/useColorScheme';
6import { parseAtUri, toBlueskyPostUrl, leafletRkeyUrl, normalizeLeafletBasePath } from '../utils/at-uri';
7import { useAtProtoRecord } from '../hooks/useAtProtoRecord';
8
9/**
10 * Props for rendering a Leaflet document record.
11 */
12export interface LeafletDocumentProps {
13 /**
14 * DID of the Leaflet publisher.
15 */
16 did: string;
17 /**
18 * Record key of the document within the Leaflet collection.
19 */
20 rkey: string;
21 /**
22 * Optional custom renderer for advanced layouts.
23 */
24 renderer?: React.ComponentType<LeafletDocumentRendererInjectedProps>;
25 /**
26 * React node rendered before data begins loading.
27 */
28 fallback?: React.ReactNode;
29 /**
30 * Indicator rendered while data is being fetched from the PDS.
31 */
32 loadingIndicator?: React.ReactNode;
33 /**
34 * Preferred color scheme to forward to the renderer.
35 */
36 colorScheme?: ColorSchemePreference;
37}
38
39/**
40 * Props provided to renderer overrides for Leaflet documents.
41 */
42export type LeafletDocumentRendererInjectedProps = LeafletDocumentRendererProps;
43
44/** NSID for Leaflet document records. */
45export const LEAFLET_DOCUMENT_COLLECTION = 'pub.leaflet.document';
46
47/**
48 * Loads a Leaflet document along with its associated publication record and renders it
49 * using the provided or default renderer.
50 *
51 * @param did - DID of the Leaflet publisher.
52 * @param rkey - Record key of the Leaflet document.
53 * @param renderer - Optional renderer override used in place of the default.
54 * @param fallback - Node rendered before loading begins.
55 * @param loadingIndicator - Node rendered while the document or publication records are loading.
56 * @param colorScheme - Preferred color scheme forwarded to the renderer.
57 * @returns A JSX subtree that renders a Leaflet document with contextual metadata.
58 */
59export const LeafletDocument: React.FC<LeafletDocumentProps> = ({ did, rkey, renderer, fallback, loadingIndicator, colorScheme }) => {
60 const Comp: React.ComponentType<LeafletDocumentRendererInjectedProps> = renderer ?? ((props) => <LeafletDocumentRenderer {...props} />);
61
62 const Wrapped: React.FC<{ record: LeafletDocumentRecord; loading: boolean; error?: Error }> = (props) => {
63 const publicationUri = useMemo(() => parseAtUri(props.record.publication), [props.record.publication]);
64 const { record: publicationRecord } = useAtProtoRecord<LeafletPublicationRecord>({
65 did: publicationUri?.did,
66 collection: publicationUri?.collection ?? 'pub.leaflet.publication',
67 rkey: publicationUri?.rkey ?? ''
68 });
69 const publicationBaseUrl = normalizeLeafletBasePath(publicationRecord?.base_path);
70 const canonicalUrl = resolveCanonicalUrl(props.record, did, rkey, publicationRecord?.base_path);
71 return (
72 <Comp
73 {...props}
74 colorScheme={colorScheme}
75 did={did}
76 rkey={rkey}
77 canonicalUrl={canonicalUrl}
78 publicationBaseUrl={publicationBaseUrl}
79 publicationRecord={publicationRecord}
80 />
81 );
82 };
83
84 return (
85 <AtProtoRecord<LeafletDocumentRecord>
86 did={did}
87 collection={LEAFLET_DOCUMENT_COLLECTION}
88 rkey={rkey}
89 renderer={Wrapped}
90 fallback={fallback}
91 loadingIndicator={loadingIndicator}
92 />
93 );
94};
95
96/**
97 * Determines the best canonical URL to expose for a Leaflet document.
98 *
99 * @param record - Leaflet document record under review.
100 * @param did - Publisher DID.
101 * @param rkey - Record key for the document.
102 * @param publicationBasePath - Optional base path configured by the publication.
103 * @returns A URL to use for canonical links.
104 */
105function resolveCanonicalUrl(record: LeafletDocumentRecord, did: string, rkey: string, publicationBasePath?: string): string {
106 const publicationUrl = leafletRkeyUrl(publicationBasePath, rkey);
107 if (publicationUrl) return publicationUrl;
108 const postUri = record.postRef?.uri;
109 if (postUri) {
110 const parsed = parseAtUri(postUri);
111 const href = parsed ? toBlueskyPostUrl(parsed) : undefined;
112 if (href) return href;
113 }
114 return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`;
115}
116
117export default LeafletDocument;