mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-16 21:14:31 +03:30
Story 3-1: Create PDF Generation API Route - Add server/api/resume/pdf.get.ts with Puppeteer integration - Support both dev (puppeteer) and prod (puppeteer-core + chromium) - Navigate to /resume?print=true for WYSIWYG capture - Return PDF with proper headers (Content-Type, Content-Disposition) - Add error handling with 500 status and JSON response - Update vercel.json with function config (memory: 1024, maxDuration: 10) Story 3-2: Create PDF Download Composable - Add app/composables/useResumePdf.ts - Implement isGenerating ref for loading state - Implement downloadPdf() with blob handling - Add toast notifications for errors - Revoke object URL to prevent memory leaks Story 3-3: Connect Download Button to PDF Generation - Update ResumeDownloadButton.vue to use useResumePdf() - Bind :loading and :disabled to isGenerating - Connect @click to downloadPdf - Remove placeholder handler Dependencies Added: - puppeteer ^24.31.0 - puppeteer-core ^24.31.0 - @sparticuz/chromium ^141.0.0 Closes Epic 3 Closes Story 3-1, 3-2, 3-3
4.7 KiB
4.7 KiB
Story 3.2: Create PDF Download Composable
Status: done
Story
As a developer, I want a composable that handles PDF download logic, so that the download button can trigger downloads easily.
Acceptance Criteria
- AC1: Given I call
downloadPdf()from the composable, when the function executes, then it fetches/api/resume/pdfas blob - AC2: The composable creates object URL from blob and triggers browser download
- AC3: Download filename is from
getPdfFilename()(e.g., "Ali_Arghyani_Resume.pdf") - AC4: The composable returns
isGenerating: Ref<boolean>for loading state - AC5: The composable returns
downloadPdf: () => Promise<void>function - AC6: Given an error occurs, when caught, then it shows toast notification with error message
- AC7: After error,
isGeneratingis set back to false - AC8: Object URL is revoked after download to prevent memory leaks
Tasks / Subtasks
-
Create composable file (AC: #4, #5)
- Create
app/composables/useResumePdf.ts - Define
isGeneratingref with initial value false - Define
downloadPdfasync function - Return both from composable
- Create
-
Implement download logic (AC: #1-#3, #8)
- Set
isGenerating = trueat start - Fetch
/api/resume/pdfwithresponseType: 'blob' - Create object URL from blob:
URL.createObjectURL(response) - Create temporary
<a>element - Set
hrefto object URL - Set
downloadattribute to filename fromgetPdfFilename() - Trigger click on element
- Revoke object URL:
URL.revokeObjectURL(url) - Set
isGenerating = falsein finally block
- Set
-
Add error handling (AC: #6, #7)
- Wrap in try-catch block
- Import
useToast()from Nuxt UI - Show error toast on catch
- Log error to console
- Ensure
isGenerating = falsein finally
-
Test composable
- Import in component and call
downloadPdf() - Verify loading state changes
- Verify file downloads with correct name
- Test error handling (disconnect network)
- Import in component and call
Dev Notes
Architecture Alignment
From Architecture Doc:
- File location:
app/composables/useResumePdf.ts - Uses
$fetchwith blob response type - Uses Nuxt UI
useToast()for notifications - Gets filename from
useResumeData().getPdfFilename()
From Tech Spec Epic 3:
- AC5-AC7 map to this story
- Composable interface defined in spec
Implementation Notes
Composable Structure:
export function useResumePdf() {
const isGenerating = ref(false)
const toast = useToast()
const { getPdfFilename } = useResumeData()
async function downloadPdf() {
isGenerating.value = true
try {
const response = await $fetch('/api/resume/pdf', {
responseType: 'blob'
})
const url = URL.createObjectURL(response)
const a = document.createElement('a')
a.href = url
a.download = getPdfFilename()
a.click()
URL.revokeObjectURL(url)
} catch (error) {
console.error('PDF generation failed:', error)
toast.add({
title: 'Error generating PDF',
description: 'Please try again',
color: 'error'
})
} finally {
isGenerating.value = false
}
}
return { isGenerating, downloadPdf }
}
Usage in Component:
<script setup>
const { isGenerating, downloadPdf } = useResumePdf()
</script>
<template>
<UButton
:loading="isGenerating"
:disabled="isGenerating"
@click="downloadPdf"
>
Download PDF
</UButton>
</template>
Dependencies
useResumeData()composable (Epic 1) - forgetPdfFilename()useToast()from Nuxt UI - for error notifications$fetchfrom Nuxt - for API calls
Testing Checklist
- Composable exports
isGeneratinganddownloadPdf isGeneratingstarts as falseisGeneratingbecomes true during downloadisGeneratingreturns to false after completion- PDF downloads with correct filename
- Error shows toast notification
- Error logs to console
isGeneratingreturns to false after error
References
- [Source: docs/architecture.md#Error-Handling]
- [Source: docs/architecture.md#Implementation-Patterns]
- [Source: docs/sprint-artifacts/tech-spec-epic-3.md#AC5-AC7]
Dev Agent Record
Context Reference
Agent Model Used
Debug Log References
Completion Notes List
File List
Change Log:
- 2025-12-01: Story drafted by SM agent (Bob)