Files
portfolio/docs/sprint-artifacts/3-2-create-pdf-download-composable.md
T
mahdiarghyani 9c40914d6a feat(resume): complete Epic 4 - Resume Content Strategy and Guidelines
- Update resume data with comprehensive professional summary and optimized work experience highlights
- Add resume content tech-spec and strategy documentation for 2025 best practices
- Create resume guidelines reference document for content consistency
- Enhance PDF composable with openPdf preview function alongside downloadPdf
- Expand CSS banner hiding rules to block certificates.dev promotional content
- Add research documentation on resume best practices and ATS optimization
- Update i18n translations for resume content in English and Persian
- Include sample Ali Arghyani resume PDF template in design assets
- Refactor resume.en.ts with improved professional positioning and achievement metrics
- Update API route documentation for PDF generation story artifacts
- Establish foundation for Epic 4 sprint planning with tech specifications
2025-12-02 12:54:26 +03:30

5.3 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

  1. AC1: Given I call downloadPdf() from the composable, when the function executes, then it fetches /api/resume/pdf as blob
  2. AC2: The composable creates object URL from blob and triggers browser download
  3. AC3: Download filename is from getPdfFilename() (e.g., "Ali_Arghyani_Resume.pdf")
  4. AC4: The composable returns isGenerating: Ref<boolean> for loading state
  5. AC5: The composable returns downloadPdf: () => Promise<void> function
  6. AC6: Given an error occurs, when caught, then it shows toast notification with error message
  7. AC7: After error, isGenerating is set back to false
  8. 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 isGenerating ref with initial value false
    • Define downloadPdf async function
    • Return both from composable
  • Implement download logic (AC: #1-#3, #8)

    • Set isGenerating = true at start
    • Fetch /api/resume/pdf with responseType: 'blob'
    • Create object URL from blob: URL.createObjectURL(response)
    • Create temporary <a> element
    • Set href to object URL
    • Set download attribute to filename from getPdfFilename()
    • Trigger click on element
    • Revoke object URL: URL.revokeObjectURL(url)
    • Set isGenerating = false in finally block
  • 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 = false in finally
  • Test composable

    • Import in component and call downloadPdf()
    • Verify loading state changes
    • Verify file downloads with correct name
    • Test error handling (disconnect network)

Dev Notes

Architecture Alignment

From Architecture Doc:

  • File location: app/composables/useResumePdf.ts
  • Uses $fetch with 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) - for getPdfFilename()
  • useToast() from Nuxt UI - for error notifications
  • $fetch from Nuxt - for API calls

Testing Checklist

  • Composable exports isGenerating and downloadPdf
  • isGenerating starts as false
  • isGenerating becomes true during download
  • isGenerating returns to false after completion
  • PDF downloads with correct filename
  • Error shows toast notification
  • Error logs to console
  • isGenerating returns 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

  • docs/sprint-artifacts/3-2-create-pdf-download-composable.context.xml

Agent Model Used

  • Claude Sonnet 4 (via Kiro)

Debug Log References

  • IDM (Internet Download Manager) intercept issues resolved
  • Changed from blob fetch to window.open approach

Completion Notes List

  • Composable created at app/composables/useResumePdf.ts
  • isGenerating ref for loading state
  • openPdf() function - opens PDF in new tab for preview
  • downloadPdf() function - forces download with attachment header
  • Uses window.open instead of $fetch to avoid IDM conflicts
  • Filename from useResumeData().getPdfFilename()
  • ⚠️ Toast error handling removed (IDM intercepts cause false errors)

File List

  • app/composables/useResumePdf.ts (created)

Change Log:

  • 2025-12-01: Story drafted by SM agent (Bob)
  • 2025-12-01: Implementation completed with blob fetch approach
  • 2025-12-01: Refactored to window.open approach due to IDM conflicts