All files / web/src/app/api/create/calendar/generate route.ts

0% Statements 0/139
0% Branches 0/1
0% Functions 0/1
0% Lines 0/139

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140                                                                                                                                                                                                                                                                                       
import { NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import { generateMonthlyTypst, generateDailyTypst, getDaysInMonth } from '../utils/typstGenerator'
import type { AbacusConfig } from '@soroban/abacus-react'
import { generateCalendarComposite } from '@/utils/calendar/generateCalendarComposite'
import { generateAbacusElement } from '@/utils/calendar/generateCalendarAbacus'
import { withAuth } from '@/lib/auth/withAuth'

interface CalendarRequest {
  month: number
  year: number
  format: 'monthly' | 'daily'
  paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
  abacusConfig?: AbacusConfig
}

export const POST = withAuth(async (request) => {
  let tempDir: string | null = null

  try {
    // Dynamic import to avoid Next.js bundler issues with react-dom/server
    const { renderToStaticMarkup } = await import('react-dom/server')

    const body: CalendarRequest = await request.json()
    const { month, year, format, paperSize, abacusConfig } = body

    // Validate inputs
    if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
      return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
    }

    // Create temp directory for SVG files
    tempDir = join(tmpdir(), `calendar-${Date.now()}-${Math.random()}`)
    mkdirSync(tempDir, { recursive: true })

    // Generate and write SVG files
    const daysInMonth = getDaysInMonth(year, month)
    let typstContent: string

    if (format === 'monthly') {
      // Generate single composite SVG for monthly calendar
      const calendarSvg = generateCalendarComposite({
        month,
        year,
        renderToString: renderToStaticMarkup,
      })
      if (!calendarSvg || calendarSvg.trim().length === 0) {
        throw new Error('Generated empty composite calendar SVG')
      }
      writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)

      // Generate Typst document
      typstContent = generateMonthlyTypst({
        month,
        year,
        paperSize,
        daysInMonth,
      })
    } else {
      // Daily format: generate individual SVGs for each day
      for (let day = 1; day <= daysInMonth; day++) {
        const svg = renderToStaticMarkup(generateAbacusElement(day, 2))
        if (!svg || svg.trim().length === 0) {
          throw new Error(`Generated empty SVG for day ${day}`)
        }
        writeFileSync(join(tempDir, `day-${day}.svg`), svg)
      }

      // Generate year SVG
      const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
      const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
      if (!yearSvg || yearSvg.trim().length === 0) {
        throw new Error(`Generated empty SVG for year ${year}`)
      }
      writeFileSync(join(tempDir, 'year.svg'), yearSvg)

      // Generate Typst document
      typstContent = generateDailyTypst({
        month,
        year,
        paperSize,
        daysInMonth,
      })
    }

    // Compile with Typst: stdin for .typ content, stdout for PDF output
    let pdfBuffer: Buffer
    try {
      pdfBuffer = execSync('typst compile --format pdf - -', {
        input: typstContent,
        cwd: tempDir, // Run in temp dir so relative paths work
        maxBuffer: 50 * 1024 * 1024, // 50MB limit for large calendars
      })
    } catch (error) {
      console.error('Typst compilation error:', error)
      return NextResponse.json(
        { error: 'Failed to compile PDF. Is Typst installed?' },
        { status: 500 }
      )
    }

    // Clean up temp directory
    rmSync(tempDir, { recursive: true, force: true })
    tempDir = null

    // Return JSON with PDF
    return NextResponse.json({
      pdf: pdfBuffer.toString('base64'),
      filename: `calendar-${year}-${String(month).padStart(2, '0')}.pdf`,
    })
  } catch (error) {
    console.error('Error generating calendar:', error)

    // Clean up temp directory if it exists
    if (tempDir) {
      try {
        rmSync(tempDir, { recursive: true, force: true })
      } catch (cleanupError) {
        console.error('Failed to clean up temp directory:', cleanupError)
      }
    }

    // Surface the actual error for debugging
    const errorMessage = error instanceof Error ? error.message : String(error)
    const errorStack = error instanceof Error ? error.stack : undefined

    return NextResponse.json(
      {
        error: 'Failed to generate calendar',
        message: errorMessage,
        ...(process.env.NODE_ENV === 'development' && { stack: errorStack }),
      },
      { status: 500 }
    )
  }
})