refactor(web): return calendar SVG preview with PDF generation

- Change generate API to return JSON instead of binary PDF
- Include base64-encoded PDF and SVG preview in response
- Update client to decode base64 PDF and trigger download
- Store SVG preview for display after generation
- Improve error handling to surface actual error messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-04 09:32:18 -06:00
parent 867c7ee172
commit 14a5de0dfa
2 changed files with 36 additions and 12 deletions

View File

@ -37,6 +37,7 @@ export async function POST(request: NextRequest) {
// Generate SVGs using server-side rendering (API routes can use react-dom/server)
const daysInMonth = getDaysInMonth(year, month)
let previewSvg: string | null = null
if (format === 'monthly') {
// Generate single composite SVG for monthly calendar (prevents multi-page overflow)
@ -49,6 +50,7 @@ export async function POST(request: NextRequest) {
if (!compositeSvg || compositeSvg.trim().length === 0) {
throw new Error(`Generated empty composite calendar SVG`)
}
previewSvg = compositeSvg
writeFileSync(join(tempDir, 'calendar.svg'), compositeSvg)
} catch (error: any) {
console.error(`Error generating composite calendar:`, error.message)
@ -121,18 +123,18 @@ export async function POST(request: NextRequest) {
)
}
// Read and return PDF
// Read PDF
const pdfBuffer = readFileSync(pdfPath)
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="calendar-${year}-${String(month).padStart(2, '0')}.pdf"`,
},
// Return JSON with both PDF and SVG preview
return NextResponse.json({
pdf: pdfBuffer.toString('base64'),
svg: previewSvg,
filename: `calendar-${year}-${String(month).padStart(2, '0')}.pdf`,
})
} catch (error) {
console.error('Error generating calendar:', error)
@ -146,6 +148,17 @@ export async function POST(request: NextRequest) {
}
}
return NextResponse.json({ error: 'Failed to generate calendar' }, { status: 500 })
// 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 }
)
}
}

View File

@ -15,6 +15,7 @@ export default function CalendarCreatorPage() {
const [format, setFormat] = useState<'monthly' | 'daily'>('monthly')
const [paperSize, setPaperSize] = useState<'us-letter' | 'a4' | 'a3' | 'tabloid'>('us-letter')
const [isGenerating, setIsGenerating] = useState(false)
const [previewSvg, setPreviewSvg] = useState<string | null>(null)
const handleGenerate = async () => {
setIsGenerating(true)
@ -34,21 +35,31 @@ export default function CalendarCreatorPage() {
})
if (!response.ok) {
throw new Error('Failed to generate calendar')
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.message || 'Failed to generate calendar')
}
const blob = await response.blob()
const data = await response.json()
// Store SVG preview for display
if (data.svg) {
setPreviewSvg(data.svg)
}
// Convert base64 PDF to blob and trigger download
const pdfBytes = Uint8Array.from(atob(data.pdf), c => c.charCodeAt(0))
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `calendar-${year}-${String(month).padStart(2, '0')}.pdf`
a.download = data.filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Error generating calendar:', error)
alert('Failed to generate calendar. Please try again.')
alert(`Failed to generate calendar: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setIsGenerating(false)
}
@ -122,7 +133,7 @@ export default function CalendarCreatorPage() {
/>
{/* Preview */}
<CalendarPreview month={month} year={year} format={format} />
<CalendarPreview month={month} year={year} format={format} previewSvg={previewSvg} />
</div>
</div>
</div>