'use client' import { Maximize2 } from 'lucide-react' import { useTranslations } from 'next-intl' import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { invoiceLabels } from '@/features/invoice-designer/Lib/invoiceLabels' import { type PdfMessages, withMarineDocumentLabels, } from '@/features/invoice-designer/Lib/marineLabels' import { buildInvoicePrintSpec } from '@/features/invoice-designer/Pdf/buildInvoicePrint' import type { DocumentSpec } from '@/features/invoice-designer/Spec/documentSpec' import { SpecThumbnail } from '@/features/invoice-designer/Render/SpecThumbnail' import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog' import type { Locale } from '@/i18n/config' import { track } from '@/lib/analytics' import { type PreviewSample, previewInvoiceInput } from '../../Lib/previewInvoice' import type { SetupDraft } from '../../Lib/setupDraft' interface LanguagePack { locale: Locale labels: Record /** The same labels with a boat yard's words: vessel, HIN, engine hours. */ marineLabels: Record sample: PreviewSample } /** The printed labels and the sample job's prose, for one language. */ async function loadPack(locale: Locale): Promise { const [pdf, onboarding] = await Promise.all([ import(`../../../../../messages/${locale}/pdf.json`), import(`../../../../../messages/${locale}/onboarding.json`), ]) const labels = invoiceLabels(pdf.default, 'invoice') return { locale, labels, marineLabels: withMarineDocumentLabels(labels, pdf.default as PdfMessages), sample: onboarding.default.setup.sample, } } /** White kept beside and below the printed content, in points. */ const GUTTER = 14 /** * The page cropped to what is printed and drawn as wide as its box: the side * margins and the empty bottom of the page are cut. A whole A4 page in a side * column prints its text at about 5px; cropped, it is nearly twice that. */ function CroppedSheet({ spec }: { spec: DocumentSpec }) { const boxRef = useRef(null) const [width, setWidth] = useState(0) useEffect(() => { const box = boxRef.current if (!box) return const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width)) observer.observe(box) return () => observer.disconnect() }, []) // The visible part of the page, in points: margins cut down to a gutter. const cropX = Math.max(0, Math.min(spec.page.margin.left, spec.page.margin.right) - GUTTER) const cropTop = Math.max(0, spec.page.margin.top - GUTTER) const scale = width ? width / (spec.page.width - cropX * 2) : 0 // Where the printed content ends, in points, so the empty rest is not drawn. const [contentBottom, setContentBottom] = useState(0) useLayoutEffect(() => { const page = boxRef.current?.querySelector('[data-preview-page] > div > div') if (!page) return const measure = () => { let bottom = 0 for (const child of Array.from(page.children) as HTMLElement[]) { // A side rail of a framed design runs the full page and ends nothing. if (child.offsetHeight >= spec.page.height * 0.9) continue bottom = Math.max(bottom, child.offsetTop + child.offsetHeight) } setContentBottom(bottom) } measure() // A logo loading or web fonts arriving change the content's height after // it is drawn, so it is measured again whenever its size changes. const observer = new ResizeObserver(measure) for (const child of Array.from(page.children)) observer.observe(child) void document.fonts?.ready.then(measure) return () => observer.disconnect() }, [spec, scale]) const visibleHeight = Math.min(spec.page.height, (contentBottom || spec.page.height) + GUTTER) - cropTop return (
{scale > 0 && (
)}
) } /** * The first invoice, drawn by the same builder and renderer as the real one, * in the language customers get. It follows every choice as it is made, and * opens at full size for a closer look. */ export function SetupPreview({ draft, caption }: { draft: SetupDraft; caption: string }) { const t = useTranslations('onboarding.setup.preview') const [pack, setPack] = useState(null) const [enlarged, setEnlarged] = useState(false) useEffect(() => { let alive = true loadPack(draft.locale) .then((next) => alive && setPack(next)) .catch(() => undefined) return () => { alive = false } }, [draft.locale]) const spec = useMemo( () => pack ? buildInvoicePrintSpec( previewInvoiceInput( draft, draft.kind === 'marine' ? pack.marineLabels : pack.labels, pack.sample ) ) : null, [draft, pack] ) return ( ) }