'use client'; import { useState, useMemo } from 'react'; import Image from 'next/image'; import dynamic from 'next/dynamic'; import { Lightbox, type LightboxWatermark } from '@/components/Lightbox'; import { useProofing } from '@/components/useProofing'; import { ProofingProvider, ProofingModal } from '@/components/ProofingLazy'; import { FadeIn } from '@/components/FadeIn'; import { escapeHtml } from '@/lib/escapeHtml'; import type { ParsedEssay, EssayBlock } from '@/lib/essay'; import { renderInlineMarkdown } from '@/lib/essay'; import { essayPhotoSequence } from '@/lib/essaySequence'; import type { PhotoItem } from './PhotoGrid'; import './essay.css'; import { useDictionary } from '@/components/I18nProvider'; import { formatJournalDate, journalDateTime } from '@/lib/journalDate'; /** * The map block is the only thing in an essay that needs Leaflet, and most * essays have none. Imported statically, LeafletMap put its wrapper and * Leaflet's stylesheet (render-blocking CSS) on every album, subpage and * journal page. The map only ever drew on the client — Leaflet itself was * already imported inside an effect — so the server renders the same empty * container it rendered before, and the wrapper arrives with the map. */ const LeafletMap = dynamic(() => import('@/components/LeafletMap').then((m) => m.LeafletMap), { ssr: false, loading: () =>
, }); interface EssayViewProps { essay: ParsedEssay; assets: PhotoItem[]; title?: string; subtitle?: string; watermark?: LightboxWatermark; /** * Client proofing — favorite hearts, the selection bar and the send-off modal. * Off by default: proofing is a delivery workflow for album handovers, and a * published story is not one. Album views opt in. */ proofing?: boolean; /** Offer the "send by email" button in the proofing modal. */ allowMailto?: boolean; /** `encodeEmail()` of the proofing recipient, when one is configured (#736). */ encodedMailto?: string; } function EssayViewContent({ essay, assets, title, subtitle, watermark, proofing: proofingEnabled = false, }: EssayViewProps) { const t = useDictionary(); const proofing = useProofing(); const [lightboxIndex, setLightboxIndex] = useState{block.author && ( )}); case 'photo': { const resolved = assetMap.get(block.assetId); if (!resolved) return null; const { item } = resolved; const isFav = proofing ? proofing.isFavorite(item.id) : false; return (
{displaySubtitle}
} {(essay.frontmatter.author || dateText) && (