// Journey Photo Book PDF — Polarsteps-inspired, magazine-density import { marked } from 'marked' import type { JourneyDetail, JourneyEntry, JourneyPhoto } from '../../store/journeyStore' function esc(str: string | null | undefined): string { if (!str) return '' return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } function md(str: string | null | undefined): string { if (!str) return '' return marked.parse(str, { async: false, breaks: true }) as string } function abs(url: string | null | undefined): string { if (!url) return '' if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) return url return window.location.origin + (url.startsWith('/') ? '' : '/') + url } function pSrc(p: JourneyPhoto): string { if (p.provider === 'local') return abs(`/uploads/${p.file_path}`) return abs(`/api/integrations/memories/${p.provider}/assets/0/${p.asset_id}/${p.owner_id}/original`) } function fmtDate(d: string): string { const date = new Date(d + 'T00:00:00') return date.toLocaleDateString('en', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) } function fmtShort(d: string): string { return new Date(d + 'T00:00:00').toLocaleDateString('en', { month: 'short', day: 'numeric' }) } function groupByDate(entries: JourneyEntry[]): Map { const groups = new Map() for (const e of entries) { if (!e.entry_date) continue if (!groups.has(e.entry_date)) groups.set(e.entry_date, []) groups.get(e.entry_date)!.push(e) } return groups } function renderProscons(entry: JourneyEntry): string { const pc = entry.pros_cons if (!pc) return '' const pros = pc.pros?.filter(p => p.trim()) || [] const cons = pc.cons?.filter(c => c.trim()) || [] if (pros.length === 0 && cons.length === 0) return '' return `
${pros.length > 0 ? `
Loved it
    ${pros.map(p => `
  • ${esc(p)}
  • `).join('')}
` : ''} ${cons.length > 0 ? `
Could be better
    ${cons.map(c => `
  • ${esc(c)}
  • `).join('')}
` : ''}
` } function renderPhotoBlock(photos: JourneyPhoto[]): string { if (photos.length === 0) return '' if (photos.length === 1) { return `
` } if (photos.length === 2) { return `
${photos.map(p => `
`).join('')}
` } // 3+ photos: hero left + stack right return `
` } export async function downloadJourneyBookPDF(journey: JourneyDetail) { const entries = (journey.entries || []).filter(e => e.type !== 'skeleton' && e.type !== 'gallery') const allPhotos = entries.flatMap(e => e.photos || []) const coverUrl = journey.cover_image ? abs(`/uploads/${journey.cover_image}`) : (allPhotos[0] ? pSrc(allPhotos[0]) : '') const grouped = groupByDate(entries) const dates = [...grouped.keys()].sort() // Build entry pages — one per entry, day header inline on first entry of day const entryPages: string[] = [] let pageNum = 1 // cover=1 dates.forEach((date, di) => { const dayEntries = grouped.get(date)! dayEntries.forEach((entry, ei) => { pageNum++ const isFirstOfDay = ei === 0 const photos = entry.photos || [] const meta = [entry.entry_time, entry.location_name].filter(Boolean).join(' · ') // Day header (inline, only on first entry of day) const dayHeaderHtml = isFirstOfDay ? `
Day ${di + 1} · ${fmtDate(date)}
` : '' // Photo block const photoHtml = renderPhotoBlock(photos) // Pros/cons const prosconsHtml = renderProscons(entry) // Story (markdown) const storyHtml = entry.story ? `
${md(entry.story)}
` : '' entryPages.push(`
${dayHeaderHtml} ${photoHtml}
${meta ? `` : ''} ${entry.title ? `

${esc(entry.title)}

` : ''} ${storyHtml} ${prosconsHtml}
`) }) }) const totalPages = pageNum + 1 // +1 for closing page const html = ` ${esc(journey.title)} — Journey Book
${coverUrl ? `
` : ''}
Journey Book

${esc(journey.title)}

${journey.subtitle ? `
${esc(journey.subtitle)}
` : ''}
${dates.length}
Days
${entries.length}
Entries
${allPhotos.length}
Photos
${entryPages.join('\n')}
The End
Made with TREK · ${new Date().getFullYear()}
` const win = window.open('', '_blank') if (!win) return win.document.write(html) win.document.close() }