'use client'; import { useState, useMemo } from 'react'; import Image from 'next/image'; import { Lightbox, type LightboxWatermark } from '@/components/Lightbox'; import { ProofingProvider, useProofing } from '@/components/ProofingContext'; import { ProofingModal } from '@/components/ProofingModal'; import { FadeIn } from '@/components/FadeIn'; import type { ParsedEssay, EssayBlock } from '@/lib/essay'; import type { PhotoItem } from './PhotoGrid'; import './essay.css'; 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; } function EssayViewContent({ essay, assets, title, subtitle, watermark, proofing: proofingEnabled = false, }: EssayViewProps) { const proofing = useProofing(); const [lightboxIndex, setLightboxIndex] = useState(null); // Map assetId / index to PhotoItem and its index in `assets` const assetMap = useMemo(() => { const map = new Map(); assets.forEach((item, index) => { map.set(item.id, { item, index }); // Also map by 1-based index string ("1", "2") for convenience map.set(`${index + 1}`, { item, index }); }); return map; }, [assets]); const displayTitle = title || essay.frontmatter.title; const displaySubtitle = subtitle || essay.frontmatter.subtitle; // Cover photo if specified const coverAsset = essay.frontmatter.coverAssetId ? assetMap.get(essay.frontmatter.coverAssetId) : undefined; const renderBlock = (block: EssayBlock, idx: number) => { switch (block.type) { case 'heading': { const Tag = `h${Math.min(block.level + 1, 4)}` as keyof React.JSX.IntrinsicElements; return ( {block.text} ); } case 'paragraph': return (
); case 'quote': return (
{block.author && — {block.author}}
); case 'photo': { const resolved = assetMap.get(block.assetId); if (!resolved) return null; const { item, index } = resolved; const isFav = proofing ? proofing.isFavorite(item.id) : false; return (
setLightboxIndex(index)} style={{ ...(item.dominantColor ? { backgroundColor: item.dominantColor } : {}), ...(item.aspectRatio ? { aspectRatio: `${item.aspectRatio}` } : {}), }} > {block.caption {proofing && ( )}
{block.caption && (
)}
); } case 'photo-pair': { const res1 = assetMap.get(block.assetIds[0]); const res2 = assetMap.get(block.assetIds[1]); if (!res1 || !res2) return null; return (
{[res1, res2].map(({ item, index }, pIdx) => { const isFav = proofing ? proofing.isFavorite(item.id) : false; return (
setLightboxIndex(index)} style={{ ...(item.dominantColor ? { backgroundColor: item.dominantColor } : {}), // Width proportional to the ratio — see .essay-pair-grid. flexGrow: item.aspectRatio ?? 1.5, aspectRatio: `${item.aspectRatio ?? 1.5}`, }} > {proofing && ( )}
); })}
{block.caption && (
)}
); } default: return null; } }; return (
{displayTitle &&

{displayTitle}

} {displaySubtitle &&

{displaySubtitle}

} {(essay.frontmatter.author || essay.frontmatter.date) && (
{[essay.frontmatter.author, essay.frontmatter.date].filter(Boolean).join(' • ')}
)}
{coverAsset && (
setLightboxIndex(coverAsset.index)} style={{ ...(coverAsset.item.dominantColor ? { backgroundColor: coverAsset.item.dominantColor } : {}), ...(coverAsset.item.aspectRatio ? { aspectRatio: `${coverAsset.item.aspectRatio}` } : {}), }} >
)}
{essay.blocks.map(renderBlock)}
{proofing && proofing.favorites.size > 0 && (
)} {proofingEnabled && } {lightboxIndex !== null && ( setLightboxIndex(null)} onNext={() => setLightboxIndex((prev) => (prev !== null ? (prev + 1) % assets.length : null)) } onPrev={() => setLightboxIndex((prev) => prev !== null ? (prev - 1 + assets.length) % assets.length : null, ) } watermark={watermark} showExifToggle={false} /> )}
); } export function EssayView(props: EssayViewProps) { const albumTokens = useMemo(() => props.assets.map((a) => a.id), [props.assets]); // Without the provider useProofing() returns null, and every proofing control // (hearts in the grid and in the lightbox, selection bar) drops out on its own. if (!props.proofing) { return ; } return ( ); }