mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-15 20:52:49 +03:30
- 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
176 lines
5.3 KiB
Markdown
176 lines
5.3 KiB
Markdown
# 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:**
|
|
```typescript
|
|
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:**
|
|
```vue
|
|
<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
|