Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories

F015 - Multi-format Export System

Objective

Enable authors to export their analysis reports as PDF or HTML files for offline viewing, sharing, and archival purposes.

Quick Implementation

Using NextSaaS Components

  • API route patterns for file generation
  • Response streaming for large files
  • Authentication middleware
  • Error handling utilities

New Requirements

  • PDF generation library
  • HTML templating
  • File streaming
  • Export tracking

MVP Implementation

1. Package Installation

# In packages/export directory npm install @react-pdf/renderer puppeteer-core chrome-aws-lambda

2. Database Schema

-- Export tracking CREATE TABLE analyzer.report_exports ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), report_id UUID REFERENCES analyzer.analysis_reports(id), user_id UUID REFERENCES auth.users(id), format VARCHAR(20) NOT NULL, -- 'pdf', 'html' file_size INTEGER, download_url TEXT, expires_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_report_exports_user_id ON analyzer.report_exports(user_id); CREATE INDEX idx_report_exports_expires_at ON analyzer.report_exports(expires_at);

3. PDF Export Service

// packages/export/src/services/pdf-exporter.ts import { Document, Page, Text, View, StyleSheet, pdf } from '@react-pdf/renderer' import { createClient } from '@mystoryflow/database/server' // PDF Styles const styles = StyleSheet.create({ page: { padding: 40, fontFamily: 'Helvetica' }, header: { marginBottom: 20, textAlign: 'center' }, title: { fontSize: 24, marginBottom: 10, fontWeight: 'bold' }, subtitle: { fontSize: 16, color: '#666', marginBottom: 5 }, section: { marginTop: 20, marginBottom: 15 }, sectionTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 10, color: '#333' }, paragraph: { fontSize: 11, lineHeight: 1.6, marginBottom: 10, textAlign: 'justify' }, scoreBox: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, bigScore: { fontSize: 48, fontWeight: 'bold', marginRight: 20 }, list: { marginLeft: 20, marginBottom: 10 }, listItem: { fontSize: 11, marginBottom: 5 } }) // PDF Document Component const AnalysisReportPDF = ({ data }: { data: any }) => ( <Document> <Page size="A4" style={styles.page}> {/* Header */} <View style={styles.header}> <Text style={styles.title}>Manuscript Analysis Report</Text> <Text style={styles.subtitle}>{data.manuscript.title}</Text> <Text style={{ fontSize: 10, color: '#999' }}> {data.manuscript.wordCount.toLocaleString()} words • {data.manuscript.genre} • {new Date().toLocaleDateString()} </Text> </View> {/* Executive Summary */} <View style={styles.section}> <Text style={styles.sectionTitle}>Executive Summary</Text> <View style={styles.scoreBox}> <Text style={styles.bigScore}>{data.overallScore}%</Text> <View> <Text style={{ fontSize: 14, fontWeight: 'bold' }}> {data.overallScore >= 85 ? 'Publication Ready' : data.overallScore >= 70 ? 'Needs Revision' : 'Major Work Needed'} </Text> <Text style={{ fontSize: 10, color: '#666' }}> Overall Manuscript Score </Text> </View> </View> <Text style={styles.paragraph}> Your {data.manuscript.genre} manuscript demonstrates several strengths while also presenting opportunities for improvement. This comprehensive analysis evaluates your work across multiple dimensions to provide actionable insights for enhancement. </Text> </View> {/* Strengths */} <View style={styles.section}> <Text style={styles.sectionTitle}>Key Strengths</Text> <View style={styles.list}> {data.topStrengths.map((strength: string, i: number) => ( <Text key={i} style={styles.listItem}>• {strength}</Text> ))} </View> </View> {/* Areas for Improvement */} <View style={styles.section}> <Text style={styles.sectionTitle}>Areas for Improvement</Text> <View style={styles.list}> {data.topWeaknesses.map((weakness: string, i: number) => ( <Text key={i} style={styles.listItem}>• {weakness}</Text> ))} </View> </View> {/* Category Scores */} <View style={styles.section}> <Text style={styles.sectionTitle}>Category Breakdown</Text> {Object.entries(data.categoryScores).map(([category, score]) => ( <View key={category} style={{ marginBottom: 8 }}> <Text style={{ fontSize: 12 }}> {category.replace(/_/g, ' ').toUpperCase()}: {score}% </Text> </View> ))} </View> </Page> {/* Additional pages for detailed analysis */} <Page size="A4" style={styles.page}> <Text style={styles.sectionTitle}>Detailed Analysis</Text> {/* Add more detailed sections here */} <View style={styles.section}> <Text style={styles.paragraph}> {/* Detailed analysis content */} </Text> </View> </Page> </Document> ) export class PDFExporter { private supabase = getSupabaseBrowserClient() async exportToPDF(reportId: string, userId: string): Promise<Buffer> { // Get report data const { data: report } = await this.supabase .from('analyzer.analysis_reports') .select('*') .eq('id', reportId) .single() if (!report) throw new Error('Report not found') // Generate PDF const doc = <AnalysisReportPDF data={report.report_data} /> const pdfBuffer = await pdf(doc).toBuffer() // Track export await this.trackExport(reportId, userId, 'pdf', pdfBuffer.length) return pdfBuffer } private async trackExport( reportId: string, userId: string, format: string, fileSize: number ): Promise<void> { await this.supabase .from('analyzer.report_exports') .insert({ report_id: reportId, user_id: userId, format, file_size: fileSize }) } }

4. HTML Export Service

// packages/export/src/services/html-exporter.ts export class HTMLExporter { private supabase = getSupabaseBrowserClient() async exportToHTML(reportId: string, userId: string): Promise<string> { // Get report data const { data: report } = await this.supabase .from('analyzer.analysis_reports') .select('*') .eq('id', reportId) .single() if (!report) throw new Error('Report not found') // Generate HTML const html = this.generateHTML(report.report_data) // Track export await this.trackExport(reportId, userId, 'html', html.length) return html } private generateHTML(data: any): string { return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${data.manuscript.title} - Analysis Report</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 40px 20px; } .header { text-align: center; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #e0e0e0; } h1 { color: #2c3e50; margin-bottom: 10px; } .metadata { color: #666; font-size: 14px; } .score-box { background: #f8f9fa; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center; } .big-score { font-size: 48px; font-weight: bold; color: ${data.overallScore >= 85 ? '#28a745' : data.overallScore >= 70 ? '#ffc107' : '#dc3545'}; } .section { margin: 40px 0; } .section h2 { color: #2c3e50; border-bottom: 1px solid #e0e0e0; padding-bottom: 10px; } ul { padding-left: 20px; } li { margin-bottom: 8px; } .category-scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; } .category-item { background: #f8f9fa; padding: 15px; border-radius: 5px; } @media print { body { padding: 0; } .no-print { display: none; } } </style> </head> <body> <div class="header"> <h1>Manuscript Analysis Report</h1> <h2>${data.manuscript.title}</h2> <div class="metadata"> ${data.manuscript.wordCount.toLocaleString()} words • ${data.manuscript.genre} • ${new Date().toLocaleDateString()} </div> </div> <div class="score-box"> <div class="big-score">${data.overallScore}%</div> <div>Overall Manuscript Score</div> <strong> ${data.overallScore >= 85 ? 'Publication Ready' : data.overallScore >= 70 ? 'Needs Revision' : 'Major Work Needed'} </strong> </div> <div class="section"> <h2>Executive Summary</h2> <p>Your ${data.manuscript.genre} manuscript demonstrates several strengths while also presenting opportunities for improvement.</p> </div> <div class="section"> <h2>Key Strengths</h2> <ul> ${data.topStrengths.map((s: string) => `<li>${s}</li>`).join('')} </ul> </div> <div class="section"> <h2>Areas for Improvement</h2> <ul> ${data.topWeaknesses.map((w: string) => `<li>${w}</li>`).join('')} </ul> </div> <div class="section"> <h2>Category Scores</h2> <div class="category-scores"> ${Object.entries(data.categoryScores).map(([cat, score]) => ` <div class="category-item"> <strong>${cat.replace(/_/g, ' ').toUpperCase()}</strong> <div>${score}%</div> </div> `).join('')} </div> </div> <div class="no-print" style="margin-top: 40px; text-align: center;"> <button onclick="window.print()">Print Report</button> </div> </body> </html> ` } }

5. Export API Routes

// apps/analyzer-app/src/app/api/reports/[id]/export/route.ts import { NextRequest, NextResponse } from 'next/server' import { PDFExporter, HTMLExporter } from '@/packages/export' import { withAuth } from '@/lib/auth' export async function GET( req: NextRequest, { params }: { params: { id: string } } ) { const session = await withAuth(req) if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const format = req.nextUrl.searchParams.get('format') || 'pdf' try { if (format === 'pdf') { const exporter = new PDFExporter() const pdfBuffer = await exporter.exportToPDF(params.id, session.user.id) return new NextResponse(pdfBuffer, { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="analysis-report.pdf"` } }) } else if (format === 'html') { const exporter = new HTMLExporter() const html = await exporter.exportToHTML(params.id, session.user.id) return new NextResponse(html, { headers: { 'Content-Type': 'text/html', 'Content-Disposition': `attachment; filename="analysis-report.html"` } }) } else { return NextResponse.json( { error: 'Invalid format' }, { status: 400 } ) } } catch (error) { console.error('Export error:', error) return NextResponse.json( { error: 'Export failed' }, { status: 500 } ) } }

MVP Acceptance Criteria

  • PDF export with professional formatting
  • HTML export with inline styles
  • Authentication required for exports
  • Export tracking in database
  • Proper content headers for download
  • Mobile-friendly HTML export
  • Print-optimized styles

Post-MVP Enhancements

  • Word document export
  • Email report delivery
  • Custom branding options
  • Batch export multiple reports
  • Scheduled report generation
  • Cloud storage integration
  • Report sharing links
  • Export templates

Implementation Time

  • Development: 1.5 days
  • Testing: 0.5 days
  • Total: 2 days

Dependencies

  • F014-REPORT-TEMPLATES (report data structure)
  • Report data must be available

Next Feature

After completion, proceed to F016-REVISION-TRACKING for version comparison.