mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-14 12:12:48 +03:30
docs(epic-3): add story context XML files for all stories
Story Context Generation Complete: - 3-1: PDF Generation API Route context (8 ACs, Puppeteer interfaces) - 3-2: PDF Download Composable context (8 ACs, blob handling) - 3-3: Download Button Integration context (8 ACs, UButton binding) Each context includes: - Acceptance criteria mapping - Relevant code artifacts and snippets - Interface definitions - Constraints and dependencies - Test ideas per AC Status Updates: - All 3 stories: drafted → ready-for-dev Files Created: - docs/sprint-artifacts/3-1-create-pdf-generation-api-route.context.xml - docs/sprint-artifacts/3-2-create-pdf-download-composable.context.xml - docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.context.xml Files Modified: - docs/sprint-artifacts/sprint-status.yaml - docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md - docs/sprint-artifacts/3-2-create-pdf-download-composable.md - docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md Ready for: Dev Agent implementation
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<story-context id="3-1-create-pdf-generation-api-route" v="1.0">
|
||||
<metadata>
|
||||
<epicId>3</epicId>
|
||||
<storyId>3.1</storyId>
|
||||
<title>Create PDF Generation API Route</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-12-01</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/3-1-create-pdf-generation-api-route.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>system</asA>
|
||||
<iWant>a server endpoint that generates PDF from the resume page</iWant>
|
||||
<soThat>users get consistent, high-quality PDF output</soThat>
|
||||
<tasks>
|
||||
- Create API route file at server/api/resume/pdf.get.ts
|
||||
- Implement Puppeteer PDF generation
|
||||
- Add error handling with timeout
|
||||
- Configure for Vercel deployment
|
||||
- Test API endpoint
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Given a GET request to /api/resume/pdf, when the server processes the request, then it returns a PDF binary with Content-Type application/pdf</criterion>
|
||||
<criterion id="AC2">Response includes Content-Disposition: attachment; filename="Ali_Arghyani_Resume.pdf"</criterion>
|
||||
<criterion id="AC3">PDF matches the web preview exactly (WYSIWYG)</criterion>
|
||||
<criterion id="AC4">PDF text is selectable and copy-able (ATS-compatible)</criterion>
|
||||
<criterion id="AC5">PDF is A4 format (210mm × 297mm)</criterion>
|
||||
<criterion id="AC6">PDF generation completes in under 3 seconds</criterion>
|
||||
<criterion id="AC7">Given an error occurs, when caught, then it returns status 500 with JSON error message</criterion>
|
||||
<criterion id="AC8">Timeout is set to 10 seconds max</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>API Contracts</section>
|
||||
<snippet>GET /api/resume/pdf returns PDF binary with Content-Type: application/pdf and Content-Disposition header</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Novel Pattern: WYSIWYG PDF Export</section>
|
||||
<snippet>Puppeteer navigates to /resume?print=true, waits for networkidle0, generates PDF with format A4 and printBackground true</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Deployment Architecture</section>
|
||||
<snippet>Use puppeteer-core + @sparticuz/chromium for Vercel serverless. Memory: 1024MB, maxDuration: 10s</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-3.md</path>
|
||||
<title>Epic Technical Specification: PDF Export</title>
|
||||
<section>APIs and Interfaces</section>
|
||||
<snippet>GET /api/resume/pdf - Success returns PDF buffer, Error returns 500 with JSON { error, message }</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<file>
|
||||
<path>app/pages/resume.vue</path>
|
||||
<description>Resume page that will be captured by Puppeteer. Supports ?print=true query param.</description>
|
||||
<relevantSymbols>isPrintMode computed property</relevantSymbols>
|
||||
</file>
|
||||
<file>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
<description>Provides getPdfFilename() for generating filename</description>
|
||||
<relevantSymbols>getPdfFilename()</relevantSymbols>
|
||||
</file>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="puppeteer" version="^23.x" note="Development - full Chromium bundled" />
|
||||
<package name="puppeteer-core" version="^23.x" note="Production - no bundled Chromium" />
|
||||
<package name="@sparticuz/chromium" version="^131.x" note="Production - Vercel-optimized Chromium" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- File location must be server/api/resume/pdf.get.ts (Nuxt server route convention)
|
||||
- Must use defineEventHandler from Nuxt
|
||||
- Must detect environment for puppeteer vs puppeteer-core selection
|
||||
- Must navigate to /resume?print=true (not /resume)
|
||||
- Must wait for networkidle0 before PDF generation
|
||||
- Must close browser in finally block to prevent memory leaks
|
||||
- Timeout must be 10 seconds max
|
||||
- Memory limit 1024MB on Vercel
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>defineEventHandler</name>
|
||||
<kind>Nuxt server utility</kind>
|
||||
<signature>defineEventHandler(async (event) => { ... })</signature>
|
||||
<path>nitro/runtime</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>setResponseHeaders</name>
|
||||
<kind>Nuxt server utility</kind>
|
||||
<signature>setResponseHeaders(event, { 'Content-Type': string, 'Content-Disposition': string })</signature>
|
||||
<path>h3</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>getRequestURL</name>
|
||||
<kind>Nuxt server utility</kind>
|
||||
<signature>getRequestURL(event): URL</signature>
|
||||
<path>h3</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>puppeteer.launch</name>
|
||||
<kind>Puppeteer API</kind>
|
||||
<signature>puppeteer.launch({ headless: boolean, args?: string[], executablePath?: string }): Promise<Browser></signature>
|
||||
<path>puppeteer or puppeteer-core</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>page.pdf</name>
|
||||
<kind>Puppeteer API</kind>
|
||||
<signature>page.pdf({ format: 'A4', printBackground: boolean, margin?: object }): Promise<Buffer></signature>
|
||||
<path>puppeteer</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Nuxt server route testing. Test API response headers and PDF content.</standards>
|
||||
<locations>server/api/**/*.spec.ts, tests/</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Request /api/resume/pdf, verify Content-Type is application/pdf</idea>
|
||||
<idea ac="AC2">Verify Content-Disposition header contains correct filename</idea>
|
||||
<idea ac="AC3">Open generated PDF, compare visually to web preview</idea>
|
||||
<idea ac="AC4">Open PDF in reader, try to select and copy text</idea>
|
||||
<idea ac="AC5">Check PDF page dimensions are A4 (210mm × 297mm)</idea>
|
||||
<idea ac="AC6">Measure time from request to response, verify under 3 seconds</idea>
|
||||
<idea ac="AC7">Simulate error, verify 500 status and JSON response</idea>
|
||||
<idea ac="AC8">Verify timeout configuration in code</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -1,6 +1,6 @@
|
||||
# Story 3.1: Create PDF Generation API Route
|
||||
|
||||
Status: drafted
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<story-context id="3-2-create-pdf-download-composable" v="1.0">
|
||||
<metadata>
|
||||
<epicId>3</epicId>
|
||||
<storyId>3.2</storyId>
|
||||
<title>Create PDF Download Composable</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-12-01</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/3-2-create-pdf-download-composable.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>developer</asA>
|
||||
<iWant>a composable that handles PDF download logic</iWant>
|
||||
<soThat>the download button can trigger downloads easily</soThat>
|
||||
<tasks>
|
||||
- Create composable file at app/composables/useResumePdf.ts
|
||||
- Implement download logic with blob handling
|
||||
- Add error handling with toast notifications
|
||||
- Test composable functionality
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Given I call downloadPdf() from the composable, when the function executes, then it fetches /api/resume/pdf as blob</criterion>
|
||||
<criterion id="AC2">The composable creates object URL from blob and triggers browser download</criterion>
|
||||
<criterion id="AC3">Download filename is from getPdfFilename() (e.g., "Ali_Arghyani_Resume.pdf")</criterion>
|
||||
<criterion id="AC4">The composable returns isGenerating: Ref<boolean> for loading state</criterion>
|
||||
<criterion id="AC5">The composable returns downloadPdf: () => Promise<void> function</criterion>
|
||||
<criterion id="AC6">Given an error occurs, when caught, then it shows toast notification with error message</criterion>
|
||||
<criterion id="AC7">After error, isGenerating is set back to false</criterion>
|
||||
<criterion id="AC8">Object URL is revoked after download to prevent memory leaks</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Error Handling</section>
|
||||
<snippet>Composable pattern with isGenerating ref, try-catch, toast notifications, and finally block for cleanup</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-3.md</path>
|
||||
<title>Epic Technical Specification: PDF Export</title>
|
||||
<section>Workflows and Sequencing</section>
|
||||
<snippet>useResumePdf.ts: Set isGenerating true, fetch blob, create object URL, trigger download, revoke URL, set isGenerating false</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<file>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
<description>Existing composable that provides getPdfFilename() helper</description>
|
||||
<relevantSymbols>getPdfFilename(): string</relevantSymbols>
|
||||
<snippet>
|
||||
function getPdfFilename(): string {
|
||||
const name = resumeData.basics.name
|
||||
const filename = name.replace(/\s+/g, '_')
|
||||
return `${filename}_Resume.pdf`
|
||||
}
|
||||
</snippet>
|
||||
</file>
|
||||
<file>
|
||||
<path>server/api/resume/pdf.get.ts</path>
|
||||
<description>API endpoint that returns PDF blob (Story 3.1)</description>
|
||||
<relevantSymbols>GET /api/resume/pdf</relevantSymbols>
|
||||
</file>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="@nuxt/ui" version="^4.0.x" note="useToast() for notifications" />
|
||||
<package name="nuxt" version="^4.1.3" note="$fetch for API calls" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- File location must be app/composables/useResumePdf.ts
|
||||
- Must use $fetch with responseType: 'blob'
|
||||
- Must use useToast() from Nuxt UI for error notifications
|
||||
- Must use getPdfFilename() from useResumeData() for filename
|
||||
- Must revoke object URL after download to prevent memory leaks
|
||||
- Must set isGenerating = false in finally block (not just catch)
|
||||
- Must handle both success and error cases
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>useResumePdf</name>
|
||||
<kind>Vue composable</kind>
|
||||
<signature>function useResumePdf(): { isGenerating: Ref<boolean>, downloadPdf: () => Promise<void> }</signature>
|
||||
<path>app/composables/useResumePdf.ts</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>$fetch</name>
|
||||
<kind>Nuxt utility</kind>
|
||||
<signature>$fetch<T>(url: string, options?: { responseType: 'blob' }): Promise<T></signature>
|
||||
<path>nuxt/app</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>useToast</name>
|
||||
<kind>Nuxt UI composable</kind>
|
||||
<signature>useToast(): { add: (options: ToastOptions) => void }</signature>
|
||||
<path>@nuxt/ui</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>URL.createObjectURL</name>
|
||||
<kind>Web API</kind>
|
||||
<signature>URL.createObjectURL(blob: Blob): string</signature>
|
||||
<path>global</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>URL.revokeObjectURL</name>
|
||||
<kind>Web API</kind>
|
||||
<signature>URL.revokeObjectURL(url: string): void</signature>
|
||||
<path>global</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Vue composable testing with Vitest. Mock $fetch and useToast.</standards>
|
||||
<locations>app/composables/**/*.spec.ts, tests/</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Mock $fetch, call downloadPdf(), verify fetch called with correct URL and responseType</idea>
|
||||
<idea ac="AC2">Verify URL.createObjectURL called with blob, anchor element created and clicked</idea>
|
||||
<idea ac="AC3">Verify anchor download attribute matches getPdfFilename() output</idea>
|
||||
<idea ac="AC4">Verify isGenerating.value is Ref<boolean> and starts as false</idea>
|
||||
<idea ac="AC5">Verify downloadPdf is async function returning Promise<void></idea>
|
||||
<idea ac="AC6">Mock $fetch to throw, verify toast.add called with error message</idea>
|
||||
<idea ac="AC7">Mock $fetch to throw, verify isGenerating.value is false after error</idea>
|
||||
<idea ac="AC8">Verify URL.revokeObjectURL called after download</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -1,6 +1,6 @@
|
||||
# Story 3.2: Create PDF Download Composable
|
||||
|
||||
Status: drafted
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<story-context id="3-3-connect-download-button-to-pdf-generation" v="1.0">
|
||||
<metadata>
|
||||
<epicId>3</epicId>
|
||||
<storyId>3.3</storyId>
|
||||
<title>Connect Download Button to PDF Generation</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-12-01</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/3-3-connect-download-button-to-pdf-generation.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>user</asA>
|
||||
<iWant>to click the download button and get my PDF</iWant>
|
||||
<soThat>I can use my resume for job applications</soThat>
|
||||
<tasks>
|
||||
- Update ResumeDownloadButton component to use useResumePdf composable
|
||||
- Bind loading and disabled states to button
|
||||
- Connect click handler to downloadPdf function
|
||||
- Test full download flow
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Given I click the download button, when PDF generation starts, then the button shows loading spinner</criterion>
|
||||
<criterion id="AC2">The button is disabled during PDF generation</criterion>
|
||||
<criterion id="AC3">Given PDF generation succeeds, when the PDF is ready, then the browser downloads the file</criterion>
|
||||
<criterion id="AC4">Downloaded filename is "Ali_Arghyani_Resume.pdf"</criterion>
|
||||
<criterion id="AC5">Button returns to normal state after download completes</criterion>
|
||||
<criterion id="AC6">Given PDF generation fails, when the error occurs, then a toast notification appears</criterion>
|
||||
<criterion id="AC7">Button returns to normal state after error</criterion>
|
||||
<criterion id="AC8">Button works correctly after error (can retry)</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Loading State</section>
|
||||
<snippet>UButton with :loading="isGenerating" :disabled="isGenerating" @click="downloadPdf"</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-3.md</path>
|
||||
<title>Epic Technical Specification: PDF Export</title>
|
||||
<section>AC6: Loading State</section>
|
||||
<snippet>Button shows loading spinner, button is disabled, loading clears when complete</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<file>
|
||||
<path>app/components/resume/ResumeDownloadButton.vue</path>
|
||||
<description>Existing download button component with placeholder handler (from Story 2.5)</description>
|
||||
<relevantSymbols>handleDownload (to be replaced), isPrintMode prop</relevantSymbols>
|
||||
<snippet>
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
isPrintMode?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
function handleDownload() {
|
||||
// Placeholder - will be replaced in Epic 3 Story 3.3
|
||||
console.log('Download PDF clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
v-if="!isPrintMode"
|
||||
icon="i-heroicons-arrow-down-tray"
|
||||
size="lg"
|
||||
color="primary"
|
||||
class="fixed bottom-6 right-6 shadow-lg no-print z-50"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<span class="hidden sm:inline">Download PDF</span>
|
||||
</UButton>
|
||||
</template>
|
||||
</snippet>
|
||||
</file>
|
||||
<file>
|
||||
<path>app/composables/useResumePdf.ts</path>
|
||||
<description>Composable providing isGenerating and downloadPdf (Story 3.2)</description>
|
||||
<relevantSymbols>useResumePdf(), isGenerating, downloadPdf</relevantSymbols>
|
||||
</file>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="@nuxt/ui" version="^4.0.x" note="UButton with loading prop" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- Must preserve existing isPrintMode prop functionality
|
||||
- Must preserve existing styling (fixed position, shadow, z-index)
|
||||
- Must preserve responsive text (hidden on mobile)
|
||||
- Must remove placeholder handleDownload function
|
||||
- Must use useResumePdf() composable
|
||||
- Must bind :loading and :disabled to isGenerating
|
||||
- Must bind @click to downloadPdf
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>useResumePdf</name>
|
||||
<kind>Vue composable</kind>
|
||||
<signature>function useResumePdf(): { isGenerating: Ref<boolean>, downloadPdf: () => Promise<void> }</signature>
|
||||
<path>app/composables/useResumePdf.ts</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>UButton</name>
|
||||
<kind>Nuxt UI component</kind>
|
||||
<signature><UButton :loading="boolean" :disabled="boolean" @click="handler" /></signature>
|
||||
<path>@nuxt/ui</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Vue component testing with Vitest and @vue/test-utils. Test user interactions.</standards>
|
||||
<locations>app/components/**/*.spec.ts, tests/</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Click button, verify loading spinner appears (check loading prop or spinner element)</idea>
|
||||
<idea ac="AC2">During generation, verify button has disabled attribute</idea>
|
||||
<idea ac="AC3">Mock successful API, verify file download triggered</idea>
|
||||
<idea ac="AC4">Verify downloaded file has correct filename</idea>
|
||||
<idea ac="AC5">After success, verify loading spinner gone and button enabled</idea>
|
||||
<idea ac="AC6">Mock API error, verify toast notification appears</idea>
|
||||
<idea ac="AC7">After error, verify button returns to normal state</idea>
|
||||
<idea ac="AC8">After error, click button again, verify it works (retry)</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -1,6 +1,6 @@
|
||||
# Story 3.3: Connect Download Button to PDF Generation
|
||||
|
||||
Status: drafted
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ development_status:
|
||||
# FRs: FR10-14
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
epic-3: contexted
|
||||
3-1-create-pdf-generation-api-route: drafted
|
||||
3-2-create-pdf-download-composable: drafted
|
||||
3-3-connect-download-button-to-pdf-generation: drafted
|
||||
3-1-create-pdf-generation-api-route: ready-for-dev
|
||||
3-2-create-pdf-download-composable: ready-for-dev
|
||||
3-3-connect-download-button-to-pdf-generation: ready-for-dev
|
||||
epic-3-retrospective: optional
|
||||
|
||||
Reference in New Issue
Block a user