mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-14 12:12:48 +03:30
feat(epic-2): complete Epic 2 story preparation - all stories ready-for-dev
- Created Epic 2 tech spec (tech-spec-epic-2.md) - Drafted 5 stories for Resume Preview Page: * 2.1: Create Resume Page Route * 2.2: Create Resume Preview Container Component * 2.3: Create Resume Header & Main Content Components * 2.4: Create Resume Sidebar Components * 2.5: Create Download Button Component - Generated story context XML files for all stories - Updated sprint-status.yaml: Epic 2 contexted, all stories ready-for-dev Epic 2 covers FR5-9, FR15-25 (resume preview with two-column layout, responsive design, print styles, and ATS-compatible HTML structure)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<story-context id="2-1-create-resume-page-route" v="1.0">
|
||||
<metadata>
|
||||
<epicId>2</epicId>
|
||||
<storyId>2.1</storyId>
|
||||
<title>Create Resume Page Route</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-11-30</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/2-1-create-resume-page-route.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>user</asA>
|
||||
<iWant>to access my resume at `/resume`</iWant>
|
||||
<soThat>I can view it before downloading</soThat>
|
||||
<tasks>
|
||||
- Create resume page route (AC: #1, #2)
|
||||
- Configure page metadata (AC: #4, #5)
|
||||
- Implement print mode detection (AC: #6)
|
||||
- Add placeholder content (AC: #1)
|
||||
- Test page functionality
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Given I navigate to `/resume`, when the page loads, then I see the resume preview</criterion>
|
||||
<criterion id="AC2">The page is standalone (no site header/footer)</criterion>
|
||||
<criterion id="AC3">The page has a white background</criterion>
|
||||
<criterion id="AC4">The page title is "Resume - Ali Arghyani"</criterion>
|
||||
<criterion id="AC5">Meta tags are set for SEO (noindex for privacy)</criterion>
|
||||
<criterion id="AC6">Given I add `?print=true` query parameter, when the page loads, then the download button is hidden (for PDF generation)</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Novel Pattern: WYSIWYG PDF Export</section>
|
||||
<snippet>Single component renders both web preview and PDF source. Query Parameter: `?print=true` hides download button in PDF.</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Project Structure</section>
|
||||
<snippet>File: `app/pages/resume.vue` - /resume route (standalone page)</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
|
||||
<title>Epic Technical Specification: Resume Preview Page</title>
|
||||
<section>AC1: Standalone Resume Route</section>
|
||||
<snippet>Page is standalone (no site header/footer), page title is "Resume - Ali Arghyani", meta tag `<meta name="robots" content="noindex">` is present</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<!-- No existing code - this is the first story creating the page -->
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="nuxt" version="^4.1.3" />
|
||||
<package name="vue" version="^3.5.13" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- Use `definePageMeta({ layout: false })` for standalone page
|
||||
- Use `useHead()` or `useSeoMeta()` for metadata
|
||||
- Use `useRoute().query.print` for print mode detection
|
||||
- File location must be `app/pages/resume.vue` (Nuxt file-based routing)
|
||||
- White background: `bg-white` class
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>useRoute</name>
|
||||
<kind>Nuxt composable</kind>
|
||||
<signature>const route = useRoute(); const isPrintMode = computed(() => route.query.print === 'true')</signature>
|
||||
<path>nuxt/app</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>useHead</name>
|
||||
<kind>Nuxt composable</kind>
|
||||
<signature>useHead({ title: string, meta: Array<{ name: string, content: string }> })</signature>
|
||||
<path>nuxt/app</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Nuxt 4 testing with Vitest. Test page routing, metadata, and print mode detection.</standards>
|
||||
<locations>tests/, app/**/*.spec.ts</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Navigate to /resume and verify page renders</idea>
|
||||
<idea ac="AC2">Check that no layout is applied (standalone)</idea>
|
||||
<idea ac="AC4">Verify page title in document.title</idea>
|
||||
<idea ac="AC5">Check for noindex meta tag in DOM</idea>
|
||||
<idea ac="AC6">Test with ?print=true parameter and verify isPrintMode is true</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -0,0 +1,151 @@
|
||||
# Story 2.1: Create Resume Page Route
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
As a user,
|
||||
I want to access my resume at `/resume`,
|
||||
so that I can view it before downloading.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **AC1:** Given I navigate to `/resume`, when the page loads, then I see the resume preview
|
||||
2. **AC2:** The page is standalone (no site header/footer)
|
||||
3. **AC3:** The page has a white background
|
||||
4. **AC4:** The page title is "Resume - Ali Arghyani"
|
||||
5. **AC5:** Meta tags are set for SEO (noindex for privacy)
|
||||
6. **AC6:** Given I add `?print=true` query parameter, when the page loads, then the download button is hidden (for PDF generation)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create resume page route (AC: #1, #2)
|
||||
- [ ] Create `app/pages/resume.vue` file
|
||||
- [ ] Use `definePageMeta({ layout: false })` for standalone page
|
||||
- [ ] Add basic page structure with white background
|
||||
|
||||
- [ ] Configure page metadata (AC: #4, #5)
|
||||
- [ ] Set page title using `useHead()` or `useSeoMeta()`
|
||||
- [ ] Add `<meta name="robots" content="noindex">` for privacy
|
||||
- [ ] Ensure title format: "Resume - Ali Arghyani"
|
||||
|
||||
- [ ] Implement print mode detection (AC: #6)
|
||||
- [ ] Use `useRoute().query.print` to detect print parameter
|
||||
- [ ] Pass print mode state to child components
|
||||
- [ ] Verify download button visibility logic
|
||||
|
||||
- [ ] Add placeholder content (AC: #1)
|
||||
- [ ] Import `ResumePreview` component (will be created in Story 2.2)
|
||||
- [ ] For now, add placeholder text: "Resume Preview Coming Soon"
|
||||
- [ ] Ensure page renders without errors
|
||||
|
||||
- [ ] Test page functionality
|
||||
- [ ] Navigate to `/resume` and verify standalone layout
|
||||
- [ ] Check page title in browser tab
|
||||
- [ ] Inspect meta tags in DOM
|
||||
- [ ] Test `/resume?print=true` parameter handling
|
||||
- [ ] Verify white background styling
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File location: `app/pages/resume.vue` (confirmed in Project Structure section)
|
||||
- Layout: Use `definePageMeta({ layout: false })` for standalone page
|
||||
- Query parameter: `?print=true` hides download button for PDF generation
|
||||
- SEO: `noindex` meta tag for privacy (resume not indexed by search engines)
|
||||
|
||||
**From Tech Spec Epic 2:**
|
||||
- AC1-AC6 map directly to this story
|
||||
- Page serves dual purposes: (1) user preview, (2) PDF generation source
|
||||
- WYSIWYG approach: same page renders for web and PDF
|
||||
|
||||
### Project Structure Notes
|
||||
|
||||
**File to Create:**
|
||||
- `app/pages/resume.vue` - Main resume route
|
||||
|
||||
**Dependencies:**
|
||||
- Nuxt 4 routing (file-based)
|
||||
- `useHead()` or `useSeoMeta()` composable for metadata
|
||||
- `useRoute()` composable for query parameter detection
|
||||
|
||||
**Future Integration:**
|
||||
- Story 2.2 will create `ResumePreview.vue` component to replace placeholder
|
||||
- Story 2.5 will create `ResumeDownloadButton.vue` that respects print mode
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**Page Structure:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const isPrintMode = computed(() => route.query.print === 'true')
|
||||
|
||||
useHead({
|
||||
title: 'Resume - Ali Arghyani',
|
||||
meta: [
|
||||
{ name: 'robots', content: 'noindex' }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-white">
|
||||
<!-- Placeholder for ResumePreview component (Story 2.2) -->
|
||||
<div class="p-8 text-center">
|
||||
<h1 class="text-2xl font-bold">Resume Preview Coming Soon</h1>
|
||||
</div>
|
||||
|
||||
<!-- Download button will be added in Story 2.5 -->
|
||||
<!-- Hidden when isPrintMode is true -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Testing Checklist:**
|
||||
- [ ] Page accessible at `http://localhost:3000/resume`
|
||||
- [ ] No site navigation visible (header/footer)
|
||||
- [ ] White background applied
|
||||
- [ ] Browser tab shows "Resume - Ali Arghyani"
|
||||
- [ ] Meta tag `<meta name="robots" content="noindex">` present in DOM
|
||||
- [ ] `?print=true` parameter detected correctly
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
|
||||
- [Source: docs/architecture.md#Project-Structure]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC1-Standalone-Resume-Route]
|
||||
- [Source: docs/epics.md#Story-2.1-Create-Resume-Page-Route]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
- docs/sprint-artifacts/2-1-create-resume-page-route.context.xml
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-11-30: Story drafted by SM agent (mahdi)
|
||||
@@ -0,0 +1,147 @@
|
||||
<story-context id="2-2-create-resume-preview-container-component" v="1.0">
|
||||
<metadata>
|
||||
<epicId>2</epicId>
|
||||
<storyId>2.2</storyId>
|
||||
<title>Create Resume Preview Container Component</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-11-30</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/2-2-create-resume-preview-container-component.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>developer</asA>
|
||||
<iWant>a container component that renders the full resume</iWant>
|
||||
<soThat>I have a single source of truth for web and PDF</soThat>
|
||||
<tasks>
|
||||
- Create ResumePreview component file
|
||||
- Implement two-column layout
|
||||
- Configure container dimensions
|
||||
- Apply color scheme and typography
|
||||
- Add print styles
|
||||
- Integrate data from composable
|
||||
- Add placeholder sections
|
||||
- Test component rendering
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Two-column layout with left sidebar (35% width) and right main content (65% width)</criterion>
|
||||
<criterion id="AC2">Container has A4 aspect ratio (210mm × 297mm)</criterion>
|
||||
<criterion id="AC3">Page margins are 24px (1.5rem)</criterion>
|
||||
<criterion id="AC4">Background is white</criterion>
|
||||
<criterion id="AC5">Color scheme is blue (#2563eb) and white</criterion>
|
||||
<criterion id="AC6">Typography uses Inter font for English text</criterion>
|
||||
<criterion id="AC7">Proper heading hierarchy (h1 for name, h2 for sections)</criterion>
|
||||
<criterion id="AC8">Body text is 14px (0.875rem)</criterion>
|
||||
<criterion id="AC9">ATS-readable font sizes</criterion>
|
||||
<criterion id="AC10">Layout is responsive - desktop shows two-column side by side, mobile shows single column (sidebar on top)</criterion>
|
||||
<criterion id="AC11">Print styles included - `.no-print` class hides elements in print</criterion>
|
||||
<criterion id="AC12">Page breaks are controlled</criterion>
|
||||
<criterion id="AC13">Colors print correctly (`printBackground: true`)</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Consistency Rules - Color Scheme</section>
|
||||
<snippet>Primary (headers, icons): Blue - text-blue-600, bg-blue-600. Background: White - bg-white. Text: Dark gray - text-gray-800.</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Consistency Rules - Typography</section>
|
||||
<snippet>Name: Inter, 2rem, Bold. Section Headers: Inter, 1rem, Semibold. Body Text: Inter, 0.875rem, Normal.</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Consistency Rules - Spacing</section>
|
||||
<snippet>Page margins: 1.5rem (24px). Section gap: 1.5rem. Sidebar width: 35%. Main content width: 65%.</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
|
||||
<title>Epic Technical Specification: Resume Preview Page</title>
|
||||
<section>AC2: Two-Column Layout</section>
|
||||
<snippet>Two-column grid: Left sidebar (35% width): Contact, Skills, Education, Languages. Right main content (65% width): Header, Summary, Experience. Container has A4 aspect ratio (210mm × 297mm).</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<artifact>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
<kind>composable</kind>
|
||||
<symbol>useResumeData</symbol>
|
||||
<reason>Provides reactive access to resume data for this component</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/types/resume.ts</path>
|
||||
<kind>types</kind>
|
||||
<symbol>Resume, ResumeBasics, WorkExperience, Education, Skill, Language</symbol>
|
||||
<reason>TypeScript interfaces for resume data structure</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/data/resume.en.ts</path>
|
||||
<kind>data</kind>
|
||||
<symbol>resumeData</symbol>
|
||||
<reason>Sample resume data to display</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/pages/resume.vue</path>
|
||||
<kind>page</kind>
|
||||
<symbol>resume page</symbol>
|
||||
<reason>Will import and render this ResumePreview component</reason>
|
||||
</artifact>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="nuxt" version="^4.1.3" />
|
||||
<package name="@nuxt/ui" version="^4.0.x" />
|
||||
<package name="tailwindcss" version="^4.1.x" />
|
||||
<package name="@nuxt/fonts" version="^0.11.x" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- Use CSS Grid for two-column layout
|
||||
- Tailwind classes for all styling
|
||||
- A4 dimensions: max-w-[210mm] min-h-[297mm]
|
||||
- Margins: p-6 (1.5rem = 24px)
|
||||
- Responsive breakpoint: md:grid-cols-[35%_65%]
|
||||
- Print styles: @media print with .no-print class
|
||||
- Color palette: Blue #2563eb, White #ffffff, Gray #1f2937
|
||||
- Typography: text-sm (0.875rem) for body, text-base for headers
|
||||
- File location: app/components/resume/ResumePreview.vue
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>useResumeData</name>
|
||||
<kind>composable</kind>
|
||||
<signature>const { resume, formatDate, getFullName, getPdfFilename } = useResumeData()</signature>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>Resume</name>
|
||||
<kind>TypeScript interface</kind>
|
||||
<signature>interface Resume { basics: ResumeBasics; work: WorkExperience[]; education: Education[]; skills: Skill[]; languages?: Language[] }</signature>
|
||||
<path>app/types/resume.ts</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Nuxt 4 component testing with Vitest. Test layout, responsive behavior, and print styles.</standards>
|
||||
<locations>app/components/**/*.spec.ts</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Mount component and verify two-column grid structure</idea>
|
||||
<idea ac="AC2">Check container dimensions match A4 (210mm × 297mm)</idea>
|
||||
<idea ac="AC3">Verify padding is 1.5rem (24px)</idea>
|
||||
<idea ac="AC4">Check background color is white</idea>
|
||||
<idea ac="AC5">Verify blue color (#2563eb) is applied</idea>
|
||||
<idea ac="AC10">Test responsive behavior at mobile breakpoint</idea>
|
||||
<idea ac="AC11">Verify .no-print class exists in styles</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -0,0 +1,232 @@
|
||||
# Story 2.2: Create Resume Preview Container Component
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
As a developer,
|
||||
I want a container component that renders the full resume,
|
||||
so that I have a single source of truth for web and PDF.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **AC1:** Given the ResumePreview component is rendered, when it displays, then it shows a two-column layout with left sidebar (35% width) and right main content (65% width)
|
||||
2. **AC2:** The container has A4 aspect ratio (210mm × 297mm)
|
||||
3. **AC3:** Page margins are 24px (1.5rem)
|
||||
4. **AC4:** Background is white
|
||||
5. **AC5:** Color scheme is blue (#2563eb) and white
|
||||
6. **AC6:** Typography uses Inter font for English text
|
||||
7. **AC7:** Proper heading hierarchy (h1 for name, h2 for sections)
|
||||
8. **AC8:** Body text is 14px (0.875rem)
|
||||
9. **AC9:** ATS-readable font sizes
|
||||
10. **AC10:** Layout is responsive - desktop shows two-column side by side, mobile shows single column (sidebar on top)
|
||||
11. **AC11:** Print styles included - `.no-print` class hides elements in print
|
||||
12. **AC12:** Page breaks are controlled
|
||||
13. **AC13:** Colors print correctly (`printBackground: true`)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create ResumePreview component file (AC: #1-#13)
|
||||
- [ ] Create `app/components/resume/ResumePreview.vue`
|
||||
- [ ] Set up component structure with script setup and TypeScript
|
||||
|
||||
- [ ] Implement two-column layout (AC: #1, #10)
|
||||
- [ ] Use CSS Grid for two-column layout
|
||||
- [ ] Set sidebar width to 35%, main content to 65%
|
||||
- [ ] Add responsive breakpoint for mobile (single column)
|
||||
- [ ] Ensure sidebar appears above main content on mobile
|
||||
|
||||
- [ ] Configure container dimensions (AC: #2, #3)
|
||||
- [ ] Set container to A4 aspect ratio (210mm × 297mm)
|
||||
- [ ] Apply 24px (1.5rem) margins
|
||||
- [ ] Use Tailwind classes: `max-w-[210mm] min-h-[297mm] p-6`
|
||||
|
||||
- [ ] Apply color scheme and typography (AC: #4-#9)
|
||||
- [ ] Set white background: `bg-white`
|
||||
- [ ] Define blue primary color: `text-blue-600` (#2563eb)
|
||||
- [ ] Configure Inter font (already available via @nuxt/fonts)
|
||||
- [ ] Set body text size: `text-sm` (0.875rem)
|
||||
- [ ] Ensure proper heading hierarchy (h1, h2)
|
||||
|
||||
- [ ] Add print styles (AC: #11-#13)
|
||||
- [ ] Create `.no-print` utility class
|
||||
- [ ] Add `@media print` styles
|
||||
- [ ] Set `printBackground: true` for colors
|
||||
- [ ] Control page breaks with `break-inside-avoid`
|
||||
|
||||
- [ ] Integrate data from composable
|
||||
- [ ] Import `useResumeData()` composable
|
||||
- [ ] Access `resume` reactive reference
|
||||
- [ ] Prepare for child component integration (Stories 2.3, 2.4)
|
||||
|
||||
- [ ] Add placeholder sections
|
||||
- [ ] Add comment placeholders for child components
|
||||
- [ ] Structure: Sidebar (Contact, Skills, Education, Languages)
|
||||
- [ ] Structure: Main (Header, Summary, Experience)
|
||||
|
||||
- [ ] Test component rendering
|
||||
- [ ] Import component in `pages/resume.vue`
|
||||
- [ ] Verify two-column layout on desktop
|
||||
- [ ] Test responsive behavior on mobile
|
||||
- [ ] Check print preview (Ctrl+P)
|
||||
- [ ] Verify A4 dimensions and margins
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File location: `app/components/resume/ResumePreview.vue`
|
||||
- Layout: CSS Grid for two-column design
|
||||
- Styling: Tailwind CSS utilities
|
||||
- Data source: `useResumeData()` composable from Epic 1
|
||||
|
||||
**From Tech Spec Epic 2:**
|
||||
- AC1-AC13 map directly to this story
|
||||
- Container is the main orchestrator for all resume sections
|
||||
- WYSIWYG pattern: same component for web and PDF
|
||||
|
||||
### Learnings from Previous Story
|
||||
|
||||
**From Story 2.1 (Status: drafted)**
|
||||
- Page route created at `app/pages/resume.vue`
|
||||
- Standalone layout configured with `definePageMeta({ layout: false })`
|
||||
- Print mode detection implemented via `useRoute().query.print`
|
||||
- White background and metadata already set
|
||||
- **Integration Point**: Import `ResumePreview` component to replace placeholder
|
||||
|
||||
[Source: docs/sprint-artifacts/2-1-create-resume-page-route.md]
|
||||
|
||||
### Project Structure Notes
|
||||
|
||||
**File to Create:**
|
||||
- `app/components/resume/ResumePreview.vue` - Main container component
|
||||
|
||||
**Dependencies:**
|
||||
- `app/composables/useResumeData.ts` (Epic 1 - completed)
|
||||
- `app/types/resume.ts` (Epic 1 - completed)
|
||||
- `app/data/resume.en.ts` (Epic 1 - completed)
|
||||
- Tailwind CSS (existing)
|
||||
- @nuxt/fonts with Inter (existing)
|
||||
|
||||
**Future Integration:**
|
||||
- Story 2.3 will create Header, Summary, Experience components
|
||||
- Story 2.4 will create Contact, Skills, Education, Languages components
|
||||
- Story 2.5 will create Download Button component
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**Component Structure:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { resume } = useResumeData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-8">
|
||||
<!-- A4 Container -->
|
||||
<div class="bg-white shadow-lg max-w-[210mm] min-h-[297mm] w-full">
|
||||
<!-- Two-column grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-[35%_65%] gap-0 p-6">
|
||||
<!-- Left Sidebar -->
|
||||
<div class="bg-blue-50 p-6 space-y-6">
|
||||
<!-- Contact (Story 2.4) -->
|
||||
<!-- Skills (Story 2.4) -->
|
||||
<!-- Education (Story 2.4) -->
|
||||
<!-- Languages (Story 2.4) -->
|
||||
</div>
|
||||
|
||||
<!-- Right Main Content -->
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Header (Story 2.3) -->
|
||||
<!-- Summary (Story 2.3) -->
|
||||
<!-- Experience (Story 2.3) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.resume-container {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
* {
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Responsive Breakpoints:**
|
||||
- Desktop (md: 768px+): Two columns side by side
|
||||
- Mobile (< 768px): Single column, sidebar stacked on top
|
||||
|
||||
**Color Palette:**
|
||||
- Primary Blue: `#2563eb` (text-blue-600, bg-blue-600)
|
||||
- Background: `#ffffff` (bg-white)
|
||||
- Sidebar Background: `#eff6ff` (bg-blue-50)
|
||||
- Text: `#1f2937` (text-gray-800)
|
||||
- Secondary Text: `#6b7280` (text-gray-600)
|
||||
|
||||
**Typography Scale:**
|
||||
- Name (h1): 2rem (text-3xl), font-bold
|
||||
- Section Headers (h2): 1rem (text-base), font-semibold
|
||||
- Body Text: 0.875rem (text-sm), font-normal
|
||||
|
||||
**Testing Checklist:**
|
||||
- [ ] Component renders without errors
|
||||
- [ ] Two-column layout displays correctly on desktop
|
||||
- [ ] Single column layout on mobile (< 768px)
|
||||
- [ ] A4 dimensions maintained (210mm × 297mm)
|
||||
- [ ] Margins are 24px (1.5rem)
|
||||
- [ ] White background applied
|
||||
- [ ] Blue color scheme visible
|
||||
- [ ] Inter font loaded and applied
|
||||
- [ ] Print preview shows correct styling
|
||||
- [ ] `.no-print` class works in print mode
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Project-Structure]
|
||||
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
|
||||
- [Source: docs/architecture.md#Consistency-Rules]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC2-Two-Column-Layout]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#Detailed-Design]
|
||||
- [Source: docs/epics.md#Story-2.2-Create-Resume-Preview-Container-Component]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
- docs/sprint-artifacts/2-2-create-resume-preview-container-component.context.xml
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-11-30: Story drafted by SM agent (mahdi)
|
||||
@@ -0,0 +1,131 @@
|
||||
<story-context id="2-3-create-resume-header-main-content-components" v="1.0">
|
||||
<metadata>
|
||||
<epicId>2</epicId>
|
||||
<storyId>2.3</storyId>
|
||||
<title>Create Resume Header & Main Content Components</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-11-30</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/2-3-create-resume-header-main-content-components.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>user</asA>
|
||||
<iWant>to see my name, title, summary, and work experience prominently</iWant>
|
||||
<soThat>recruiters see the most important information first</soThat>
|
||||
<tasks>
|
||||
- Create ResumeHeader component
|
||||
- Create ResumeSummary component
|
||||
- Create ResumeExperience component
|
||||
- Integrate components into ResumePreview
|
||||
- Test components rendering
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Header shows full name in large bold text (2rem, font-bold)</criterion>
|
||||
<criterion id="AC2">Job title appears below name (1.25rem, text-gray-600)</criterion>
|
||||
<criterion id="AC3">Blue accent line or background element is present</criterion>
|
||||
<criterion id="AC4">Summary shows "Profile" or "Summary" section header</criterion>
|
||||
<criterion id="AC5">Professional summary paragraph is displayed</criterion>
|
||||
<criterion id="AC6">Line height is 1.6 for readability</criterion>
|
||||
<criterion id="AC7">Experience shows for each job: position title (bold), company name, date range (formatted: "Jan 2022 - Present"), bullet points for highlights (• character)</criterion>
|
||||
<criterion id="AC8">Jobs are sorted by date (most recent first)</criterion>
|
||||
<criterion id="AC9">"Present" is shown for current jobs (no endDate)</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Data Architecture</section>
|
||||
<snippet>WorkExperience interface: company, position, startDate (YYYY-MM), endDate (optional), highlights (string[])</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
|
||||
<title>Epic Technical Specification: Resume Preview Page</title>
|
||||
<section>AC5: Header Section, AC10: Summary Section, AC11: Experience Section</section>
|
||||
<snippet>Header: Name in 2rem bold, job title 1.25rem gray, blue accent line. Summary: Section header, paragraph with line-height 1.6. Experience: Position, company, date range, bullet highlights.</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<artifact>
|
||||
<path>app/components/resume/ResumePreview.vue</path>
|
||||
<kind>component</kind>
|
||||
<symbol>ResumePreview</symbol>
|
||||
<reason>Parent container where these components will be integrated</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
<kind>composable</kind>
|
||||
<symbol>useResumeData, formatDate</symbol>
|
||||
<reason>Provides data and date formatting helper</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/types/resume.ts</path>
|
||||
<kind>types</kind>
|
||||
<symbol>WorkExperience, ResumeBasics</symbol>
|
||||
<reason>TypeScript interfaces for props</reason>
|
||||
</artifact>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="vue" version="^3.5.13" />
|
||||
<package name="@nuxt/ui" version="^4.0.x" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- Header: text-3xl font-bold for name, text-xl text-gray-600 for title
|
||||
- Blue accent: border-b-4 border-blue-600
|
||||
- Summary: text-base font-semibold for header, leading-relaxed (1.6) for paragraph
|
||||
- Experience: Sort by startDate descending, format dates with formatDate() helper
|
||||
- Bullet character: • (U+2022)
|
||||
- File locations: app/components/resume/ResumeHeader.vue, ResumeSummary.vue, ResumeExperience.vue
|
||||
- Props pattern: Accept specific data fields, not entire resume object
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>ResumeHeader Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { name: string; label: string }</signature>
|
||||
<path>app/components/resume/ResumeHeader.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>ResumeSummary Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { summary: string }</signature>
|
||||
<path>app/components/resume/ResumeSummary.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>ResumeExperience Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { work: WorkExperience[] }</signature>
|
||||
<path>app/components/resume/ResumeExperience.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>formatDate</name>
|
||||
<kind>helper function</kind>
|
||||
<signature>formatDate(date: string, locale?: string): string</signature>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Vue component testing with Vitest. Test props, rendering, and date formatting.</standards>
|
||||
<locations>app/components/**/*.spec.ts</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Mount ResumeHeader with sample data, verify name displays with correct styling</idea>
|
||||
<idea ac="AC2">Verify job title displays below name</idea>
|
||||
<idea ac="AC3">Check for blue border element</idea>
|
||||
<idea ac="AC5">Mount ResumeSummary, verify paragraph renders</idea>
|
||||
<idea ac="AC6">Check computed line-height is 1.6</idea>
|
||||
<idea ac="AC7">Mount ResumeExperience with multiple jobs, verify all fields display</idea>
|
||||
<idea ac="AC8">Test job sorting by date</idea>
|
||||
<idea ac="AC9">Test with job missing endDate, verify "Present" displays</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -0,0 +1,257 @@
|
||||
# Story 2.3: Create Resume Header & Main Content Components
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
As a user,
|
||||
I want to see my name, title, summary, and work experience prominently,
|
||||
so that recruiters see the most important information first.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### ResumeHeader.vue
|
||||
1. **AC1:** Given the header component renders, when it displays, then it shows full name in large bold text (2rem, font-bold)
|
||||
2. **AC2:** Job title appears below name (1.25rem, text-gray-600)
|
||||
3. **AC3:** Blue accent line or background element is present
|
||||
|
||||
### ResumeSummary.vue
|
||||
4. **AC4:** Given the summary component renders, when it displays, then it shows "Profile" or "Summary" section header
|
||||
5. **AC5:** Professional summary paragraph is displayed
|
||||
6. **AC6:** Line height is 1.6 for readability
|
||||
|
||||
### ResumeExperience.vue
|
||||
7. **AC7:** Given the experience component renders, when it displays, then it shows for each job: position title (bold), company name, date range (formatted: "Jan 2022 - Present"), bullet points for highlights (• character)
|
||||
8. **AC8:** Jobs are sorted by date (most recent first)
|
||||
9. **AC9:** "Present" is shown for current jobs (no endDate)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create ResumeHeader component (AC: #1-#3)
|
||||
- [ ] Create `app/components/resume/ResumeHeader.vue`
|
||||
- [ ] Accept props: `name` (string), `label` (string)
|
||||
- [ ] Display name with `text-3xl font-bold` (2rem)
|
||||
- [ ] Display job title with `text-xl text-gray-600` (1.25rem)
|
||||
- [ ] Add blue accent line: `border-b-4 border-blue-600`
|
||||
|
||||
- [ ] Create ResumeSummary component (AC: #4-#6)
|
||||
- [ ] Create `app/components/resume/ResumeSummary.vue`
|
||||
- [ ] Accept props: `summary` (string)
|
||||
- [ ] Add section header "Profile" with `text-base font-semibold`
|
||||
- [ ] Display summary paragraph with `leading-relaxed` (line-height: 1.6)
|
||||
- [ ] Use `text-sm text-gray-800`
|
||||
|
||||
- [ ] Create ResumeExperience component (AC: #7-#9)
|
||||
- [ ] Create `app/components/resume/ResumeExperience.vue`
|
||||
- [ ] Accept props: `work` (WorkExperience[])
|
||||
- [ ] Add section header "Experience" with `text-base font-semibold`
|
||||
- [ ] Sort jobs by startDate (most recent first)
|
||||
- [ ] For each job, display:
|
||||
- Position title: `font-semibold text-gray-900`
|
||||
- Company name: `text-gray-700`
|
||||
- Date range: Use `formatDate()` helper from composable
|
||||
- Highlights: `<ul>` with bullet points (• character)
|
||||
- [ ] Handle current jobs: Show "Present" if no endDate
|
||||
|
||||
- [ ] Integrate components into ResumePreview (AC: #1-#9)
|
||||
- [ ] Import all three components in `ResumePreview.vue`
|
||||
- [ ] Pass data from `useResumeData()` composable
|
||||
- [ ] Place in right main content column:
|
||||
- ResumeHeader at top
|
||||
- ResumeSummary below header
|
||||
- ResumeExperience below summary
|
||||
|
||||
- [ ] Test components rendering
|
||||
- [ ] Verify header displays name and title correctly
|
||||
- [ ] Check blue accent line visibility
|
||||
- [ ] Verify summary section with proper line height
|
||||
- [ ] Test experience section with multiple jobs
|
||||
- [ ] Verify date formatting ("Jan 2022 - Present")
|
||||
- [ ] Check job sorting (most recent first)
|
||||
- [ ] Test with current job (no endDate)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File locations: `app/components/resume/ResumeHeader.vue`, `ResumeSummary.vue`, `ResumeExperience.vue`
|
||||
- Data source: Props passed from `ResumePreview.vue` (which uses `useResumeData()`)
|
||||
- Date formatting: Use `formatDate()` helper from composable
|
||||
|
||||
**From Tech Spec Epic 2:**
|
||||
- AC5, AC10, AC11, AC15, AC20, AC21 map to this story
|
||||
- Main content components for right column (65% width)
|
||||
- Professional typography and spacing
|
||||
|
||||
### Learnings from Previous Story
|
||||
|
||||
**From Story 2.2 (Status: drafted)**
|
||||
- `ResumePreview.vue` container created with two-column grid
|
||||
- Right main content column ready for child components
|
||||
- Data access via `useResumeData()` composable established
|
||||
- Tailwind styling patterns defined (text-sm, text-blue-600, etc.)
|
||||
- **Integration Point**: Import these components into right column of ResumePreview
|
||||
|
||||
[Source: docs/sprint-artifacts/2-2-create-resume-preview-container-component.md]
|
||||
|
||||
### Project Structure Notes
|
||||
|
||||
**Files to Create:**
|
||||
- `app/components/resume/ResumeHeader.vue`
|
||||
- `app/components/resume/ResumeSummary.vue`
|
||||
- `app/components/resume/ResumeExperience.vue`
|
||||
|
||||
**Files to Modify:**
|
||||
- `app/components/resume/ResumePreview.vue` - Import and integrate new components
|
||||
|
||||
**Dependencies:**
|
||||
- `app/composables/useResumeData.ts` - `formatDate()` helper
|
||||
- `app/types/resume.ts` - `WorkExperience` interface
|
||||
- `app/data/resume.en.ts` - Sample data
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**ResumeHeader.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
name: string
|
||||
label: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b-4 border-blue-600 pb-4 mb-6">
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ name }}</h1>
|
||||
<p class="text-xl text-gray-600 mt-1">{{ label }}</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**ResumeSummary.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
summary: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-base font-semibold text-gray-900 mb-2">Profile</h2>
|
||||
<p class="text-sm text-gray-800 leading-relaxed">{{ summary }}</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**ResumeExperience.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { WorkExperience } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
work: WorkExperience[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { formatDate } = useResumeData()
|
||||
|
||||
const sortedWork = computed(() => {
|
||||
return [...props.work].sort((a, b) => {
|
||||
const dateA = a.startDate || ''
|
||||
const dateB = b.startDate || ''
|
||||
return dateB.localeCompare(dateA) // Most recent first
|
||||
})
|
||||
})
|
||||
|
||||
const formatDateRange = (start: string, end?: string) => {
|
||||
const startFormatted = formatDate(start)
|
||||
const endFormatted = end ? formatDate(end) : 'Present'
|
||||
return `${startFormatted} - ${endFormatted}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-gray-900 mb-4">Experience</h2>
|
||||
<div v-for="job in sortedWork" :key="job.company + job.position" class="mb-6">
|
||||
<h3 class="font-semibold text-gray-900">{{ job.position }}</h3>
|
||||
<p class="text-sm text-gray-700">{{ job.company }}</p>
|
||||
<p class="text-xs text-gray-600 mb-2">{{ formatDateRange(job.startDate, job.endDate) }}</p>
|
||||
<ul class="text-sm text-gray-800 space-y-1">
|
||||
<li v-for="(highlight, idx) in job.highlights" :key="idx" class="flex">
|
||||
<span class="mr-2">•</span>
|
||||
<span>{{ highlight }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Integration in ResumePreview.vue:**
|
||||
```vue
|
||||
<!-- Right Main Content -->
|
||||
<div class="p-6 space-y-6">
|
||||
<ResumeHeader
|
||||
:name="resume.basics.name"
|
||||
:label="resume.basics.label"
|
||||
/>
|
||||
<ResumeSummary :summary="resume.basics.summary" />
|
||||
<ResumeExperience :work="resume.work" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist:**
|
||||
- [ ] Header displays name "Ali Arghyani" in large bold text
|
||||
- [ ] Job title displays below name in gray
|
||||
- [ ] Blue accent line visible under header
|
||||
- [ ] Summary section has "Profile" header
|
||||
- [ ] Summary paragraph has proper line height (1.6)
|
||||
- [ ] Experience section has "Experience" header
|
||||
- [ ] Jobs sorted by date (most recent first)
|
||||
- [ ] Each job shows position, company, date range, highlights
|
||||
- [ ] Date formatting works: "Jan 2022 - Present"
|
||||
- [ ] Bullet points (•) display correctly
|
||||
- [ ] Current job shows "Present" instead of end date
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Data-Architecture]
|
||||
- [Source: docs/architecture.md#Consistency-Rules]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC5-Header-Section]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC10-Summary-Section]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC11-Experience-Section]
|
||||
- [Source: docs/epics.md#Story-2.3-Create-Resume-Header-Main-Content-Components]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
- docs/sprint-artifacts/2-3-create-resume-header-main-content-components.context.xml
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-11-30: Story drafted by SM agent (mahdi)
|
||||
@@ -0,0 +1,140 @@
|
||||
<story-context id="2-4-create-resume-sidebar-components" v="1.0">
|
||||
<metadata>
|
||||
<epicId>2</epicId>
|
||||
<storyId>2.4</storyId>
|
||||
<title>Create Resume Sidebar Components</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-11-30</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/2-4-create-resume-sidebar-components.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>user</asA>
|
||||
<iWant>to see my contact info, skills, education, and languages in the sidebar</iWant>
|
||||
<soThat>this supporting information is easily scannable</soThat>
|
||||
<tasks>
|
||||
- Create ResumeContact component
|
||||
- Create ResumeSkills component
|
||||
- Create ResumeEducation component
|
||||
- Create ResumeLanguages component
|
||||
- Integrate components into ResumePreview
|
||||
- Test components rendering
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Contact shows email with icon (i-mdi-email)</criterion>
|
||||
<criterion id="AC2">Phone with icon (i-mdi-phone)</criterion>
|
||||
<criterion id="AC3">Location with icon (i-mdi-map-marker)</criterion>
|
||||
<criterion id="AC4">Social profiles with icons (LinkedIn, GitHub, etc.)</criterion>
|
||||
<criterion id="AC5">Each item is on its own line</criterion>
|
||||
<criterion id="AC6">Links are clickable (mailto:, tel:, https://)</criterion>
|
||||
<criterion id="AC7">Skills shows "Skills" section header</criterion>
|
||||
<criterion id="AC8">Skill categories as subheaders</criterion>
|
||||
<criterion id="AC9">Keywords as tags or comma-separated list</criterion>
|
||||
<criterion id="AC10">Education shows degree type and field (e.g., "B.Sc. Computer Science")</criterion>
|
||||
<criterion id="AC11">Institution name</criterion>
|
||||
<criterion id="AC12">Date range</criterion>
|
||||
<criterion id="AC13">Languages shows "Languages" section header</criterion>
|
||||
<criterion id="AC14">Each language with fluency level</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Data Architecture</section>
|
||||
<snippet>ResumeBasics: email, phone, location {city, country}, profiles [{network, url, icon}]. Skill: {name, keywords[]}. Education: {institution, area, studyType, startDate, endDate}. Language: {language, fluency}.</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
|
||||
<title>Epic Technical Specification: Resume Preview Page</title>
|
||||
<section>AC6-AC9: Sidebar Components</section>
|
||||
<snippet>Contact: Icons with clickable links. Skills: Categories with keywords. Education: Degree, institution, dates. Languages: Language with fluency.</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<artifact>
|
||||
<path>app/components/resume/ResumePreview.vue</path>
|
||||
<kind>component</kind>
|
||||
<symbol>ResumePreview</symbol>
|
||||
<reason>Parent container where sidebar components will be integrated</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/composables/useResumeData.ts</path>
|
||||
<kind>composable</kind>
|
||||
<symbol>useResumeData, formatDate</symbol>
|
||||
<reason>Provides data and date formatting</reason>
|
||||
</artifact>
|
||||
<artifact>
|
||||
<path>app/types/resume.ts</path>
|
||||
<kind>types</kind>
|
||||
<symbol>ResumeBasics, Skill, Education, Language</symbol>
|
||||
<reason>TypeScript interfaces for props</reason>
|
||||
</artifact>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="@nuxt/ui" version="^4.0.x" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- Use Nuxt UI UIcon component for all icons
|
||||
- Icon names: i-mdi-email, i-mdi-phone, i-mdi-map-marker, i-mdi-linkedin, i-mdi-github
|
||||
- Links: mailto: for email, tel: for phone, https:// for social profiles
|
||||
- Section headers: text-base font-semibold text-gray-900 mb-4
|
||||
- Body text: text-sm for content, text-xs for secondary info
|
||||
- File locations: app/components/resume/ResumeContact.vue, ResumeSkills.vue, ResumeEducation.vue, ResumeLanguages.vue
|
||||
- Skills keywords: Join with ', ' (comma-separated)
|
||||
- Education format: "{studyType} {area}"
|
||||
- Languages format: "{language} - {fluency}"
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>ResumeContact Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { basics: ResumeBasics }</signature>
|
||||
<path>app/components/resume/ResumeContact.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>ResumeSkills Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { skills: Skill[] }</signature>
|
||||
<path>app/components/resume/ResumeSkills.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>ResumeEducation Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { education: Education[] }</signature>
|
||||
<path>app/components/resume/ResumeEducation.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>ResumeLanguages Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { languages?: Language[] }</signature>
|
||||
<path>app/components/resume/ResumeLanguages.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>UIcon</name>
|
||||
<kind>Nuxt UI component</kind>
|
||||
<signature><UIcon name="icon-name" class="..." /></signature>
|
||||
<path>@nuxt/ui</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Vue component testing with Vitest. Test props, icon rendering, and link functionality.</standards>
|
||||
<locations>app/components/**/*.spec.ts</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1-AC6">Mount ResumeContact, verify all contact fields and icons display, test link hrefs</idea>
|
||||
<idea ac="AC7-AC9">Mount ResumeSkills, verify categories and keywords display correctly</idea>
|
||||
<idea ac="AC10-AC12">Mount ResumeEducation, verify degree format and date range</idea>
|
||||
<idea ac="AC13-AC14">Mount ResumeLanguages, verify language list with fluency levels</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -0,0 +1,324 @@
|
||||
# Story 2.4: Create Resume Sidebar Components
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
As a user,
|
||||
I want to see my contact info, skills, education, and languages in the sidebar,
|
||||
so that this supporting information is easily scannable.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### ResumeContact.vue
|
||||
1. **AC1:** Given the contact component renders, when it displays, then it shows email with icon (i-mdi-email)
|
||||
2. **AC2:** Phone with icon (i-mdi-phone)
|
||||
3. **AC3:** Location with icon (i-mdi-map-marker)
|
||||
4. **AC4:** Social profiles with icons (LinkedIn, GitHub, etc.)
|
||||
5. **AC5:** Each item is on its own line
|
||||
6. **AC6:** Links are clickable (mailto:, tel:, https://)
|
||||
|
||||
### ResumeSkills.vue
|
||||
7. **AC7:** Given the skills component renders, when it displays, then it shows "Skills" section header
|
||||
8. **AC8:** Skill categories as subheaders
|
||||
9. **AC9:** Keywords as tags or comma-separated list
|
||||
|
||||
### ResumeEducation.vue
|
||||
10. **AC10:** Given the education component renders, when it displays, then it shows degree type and field (e.g., "B.Sc. Computer Science")
|
||||
11. **AC11:** Institution name
|
||||
12. **AC12:** Date range
|
||||
|
||||
### ResumeLanguages.vue
|
||||
13. **AC13:** Given the languages component renders, when it displays, then it shows "Languages" section header
|
||||
14. **AC14:** Each language with fluency level
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create ResumeContact component (AC: #1-#6)
|
||||
- [ ] Create `app/components/resume/ResumeContact.vue`
|
||||
- [ ] Accept props: `basics` (ResumeBasics)
|
||||
- [ ] Display email with icon and mailto: link
|
||||
- [ ] Display phone with icon and tel: link
|
||||
- [ ] Display location with icon (city, country)
|
||||
- [ ] Display social profiles with icons and https:// links
|
||||
- [ ] Use Nuxt UI `UIcon` component for icons
|
||||
- [ ] Style each item on separate line
|
||||
|
||||
- [ ] Create ResumeSkills component (AC: #7-#9)
|
||||
- [ ] Create `app/components/resume/ResumeSkills.vue`
|
||||
- [ ] Accept props: `skills` (Skill[])
|
||||
- [ ] Add section header "Skills"
|
||||
- [ ] Display each skill category as subheader
|
||||
- [ ] Display keywords as comma-separated list or tags
|
||||
- [ ] Use consistent styling with other sidebar sections
|
||||
|
||||
- [ ] Create ResumeEducation component (AC: #10-#12)
|
||||
- [ ] Create `app/components/resume/ResumeEducation.vue`
|
||||
- [ ] Accept props: `education` (Education[])
|
||||
- [ ] Add section header "Education"
|
||||
- [ ] Display degree type and field (e.g., "B.Sc. Computer Science")
|
||||
- [ ] Display institution name
|
||||
- [ ] Display date range using `formatDate()` helper
|
||||
|
||||
- [ ] Create ResumeLanguages component (AC: #13-#14)
|
||||
- [ ] Create `app/components/resume/ResumeLanguages.vue`
|
||||
- [ ] Accept props: `languages` (Language[])
|
||||
- [ ] Add section header "Languages"
|
||||
- [ ] Display each language with fluency level
|
||||
- [ ] Format: "English - Fluent"
|
||||
|
||||
- [ ] Integrate components into ResumePreview (AC: #1-#14)
|
||||
- [ ] Import all four components in `ResumePreview.vue`
|
||||
- [ ] Pass data from `useResumeData()` composable
|
||||
- [ ] Place in left sidebar column:
|
||||
- ResumeContact at top
|
||||
- ResumeSkills below contact
|
||||
- ResumeEducation below skills
|
||||
- ResumeLanguages at bottom
|
||||
|
||||
- [ ] Test components rendering
|
||||
- [ ] Verify contact info displays with icons
|
||||
- [ ] Test clickable links (email, phone, social)
|
||||
- [ ] Check skills section with categories and keywords
|
||||
- [ ] Verify education section with degree and dates
|
||||
- [ ] Test languages section with fluency levels
|
||||
- [ ] Verify all components fit in sidebar layout
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File locations: `app/components/resume/ResumeContact.vue`, `ResumeSkills.vue`, `ResumeEducation.vue`, `ResumeLanguages.vue`
|
||||
- Icons: Use Nuxt UI `UIcon` component with Iconify icons
|
||||
- Data source: Props passed from `ResumePreview.vue`
|
||||
|
||||
**From Tech Spec Epic 2:**
|
||||
- AC6, AC7, AC8, AC9, AC16, AC17, AC18, AC19 map to this story
|
||||
- Sidebar components for left column (35% width)
|
||||
- Consistent section header styling
|
||||
|
||||
### Learnings from Previous Story
|
||||
|
||||
**From Story 2.3 (Status: drafted)**
|
||||
- Main content components created (Header, Summary, Experience)
|
||||
- Component prop patterns established
|
||||
- Section header styling: `text-base font-semibold text-gray-900 mb-2`
|
||||
- Date formatting via `formatDate()` helper
|
||||
- **Integration Point**: Import sidebar components into left column of ResumePreview
|
||||
|
||||
[Source: docs/sprint-artifacts/2-3-create-resume-header-main-content-components.md]
|
||||
|
||||
### Project Structure Notes
|
||||
|
||||
**Files to Create:**
|
||||
- `app/components/resume/ResumeContact.vue`
|
||||
- `app/components/resume/ResumeSkills.vue`
|
||||
- `app/components/resume/ResumeEducation.vue`
|
||||
- `app/components/resume/ResumeLanguages.vue`
|
||||
|
||||
**Files to Modify:**
|
||||
- `app/components/resume/ResumePreview.vue` - Import and integrate sidebar components
|
||||
|
||||
**Dependencies:**
|
||||
- `@nuxt/ui` - `UIcon` component
|
||||
- `app/composables/useResumeData.ts` - `formatDate()` helper
|
||||
- `app/types/resume.ts` - Type interfaces
|
||||
- `app/data/resume.en.ts` - Sample data
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**ResumeContact.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { ResumeBasics } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
basics: ResumeBasics
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-base font-semibold text-gray-900 mb-4">Contact</h2>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<UIcon name="i-mdi-email" class="text-blue-600 flex-shrink-0" />
|
||||
<a :href="`mailto:${basics.email}`" class="text-gray-700 hover:text-blue-600">
|
||||
{{ basics.email }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<UIcon name="i-mdi-phone" class="text-blue-600 flex-shrink-0" />
|
||||
<a :href="`tel:${basics.phone}`" class="text-gray-700 hover:text-blue-600">
|
||||
{{ basics.phone }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<UIcon name="i-mdi-map-marker" class="text-blue-600 flex-shrink-0" />
|
||||
<span class="text-gray-700">
|
||||
{{ basics.location.city }}, {{ basics.location.country }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Social Profiles -->
|
||||
<div v-for="profile in basics.profiles" :key="profile.network" class="flex items-center gap-2 text-sm">
|
||||
<UIcon :name="profile.icon || 'i-mdi-link'" class="text-blue-600 flex-shrink-0" />
|
||||
<a :href="profile.url" target="_blank" class="text-gray-700 hover:text-blue-600">
|
||||
{{ profile.network }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**ResumeSkills.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { Skill } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
skills: Skill[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-base font-semibold text-gray-900 mb-4">Skills</h2>
|
||||
|
||||
<div v-for="skill in skills" :key="skill.name" class="space-y-1">
|
||||
<h3 class="text-sm font-medium text-gray-800">{{ skill.name }}</h3>
|
||||
<p class="text-xs text-gray-600">
|
||||
{{ skill.keywords.join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**ResumeEducation.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { Education } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
education: Education[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const { formatDate } = useResumeData()
|
||||
|
||||
const formatDateRange = (start: string, end?: string) => {
|
||||
const startFormatted = formatDate(start)
|
||||
const endFormatted = end ? formatDate(end) : 'Present'
|
||||
return `${startFormatted} - ${endFormatted}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-base font-semibold text-gray-900 mb-4">Education</h2>
|
||||
|
||||
<div v-for="edu in education" :key="edu.institution" class="space-y-1">
|
||||
<h3 class="text-sm font-medium text-gray-800">
|
||||
{{ edu.studyType }} {{ edu.area }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-600">{{ edu.institution }}</p>
|
||||
<p class="text-xs text-gray-500">{{ formatDateRange(edu.startDate, edu.endDate) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**ResumeLanguages.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { Language } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
languages?: Language[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="languages && languages.length > 0" class="space-y-2">
|
||||
<h2 class="text-base font-semibold text-gray-900 mb-4">Languages</h2>
|
||||
|
||||
<div v-for="lang in languages" :key="lang.language" class="text-sm text-gray-700">
|
||||
{{ lang.language }} - {{ lang.fluency }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Integration in ResumePreview.vue:**
|
||||
```vue
|
||||
<!-- Left Sidebar -->
|
||||
<div class="bg-blue-50 p-6 space-y-6">
|
||||
<ResumeContact :basics="resume.basics" />
|
||||
<ResumeSkills :skills="resume.skills" />
|
||||
<ResumeEducation :education="resume.education" />
|
||||
<ResumeLanguages :languages="resume.languages" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Testing Checklist:**
|
||||
- [ ] Contact section displays email, phone, location with icons
|
||||
- [ ] Email link opens mailto: client
|
||||
- [ ] Phone link opens tel: dialer
|
||||
- [ ] Social profile links open in new tab
|
||||
- [ ] Skills section shows categories and keywords
|
||||
- [ ] Keywords displayed as comma-separated list
|
||||
- [ ] Education section shows degree, institution, dates
|
||||
- [ ] Date formatting works correctly
|
||||
- [ ] Languages section shows language and fluency
|
||||
- [ ] All components fit properly in sidebar
|
||||
- [ ] Icons render correctly (Nuxt UI Icon)
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Data-Architecture]
|
||||
- [Source: docs/architecture.md#Consistency-Rules]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC6-Contact-Section]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC7-Skills-Section]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC8-Education-Section]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC9-Languages-Section]
|
||||
- [Source: docs/epics.md#Story-2.4-Create-Resume-Sidebar-Components]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
- docs/sprint-artifacts/2-4-create-resume-sidebar-components.context.xml
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-11-30: Story drafted by SM agent (mahdi)
|
||||
@@ -0,0 +1,108 @@
|
||||
<story-context id="2-5-create-download-button-component" v="1.0">
|
||||
<metadata>
|
||||
<epicId>2</epicId>
|
||||
<storyId>2.5</storyId>
|
||||
<title>Create Download Button Component</title>
|
||||
<status>ready-for-dev</status>
|
||||
<generatedAt>2025-11-30</generatedAt>
|
||||
<generator>BMAD Story Context Workflow</generator>
|
||||
<sourceStoryPath>docs/sprint-artifacts/2-5-create-download-button-component.md</sourceStoryPath>
|
||||
</metadata>
|
||||
|
||||
<story>
|
||||
<asA>user</asA>
|
||||
<iWant>a prominent download button</iWant>
|
||||
<soThat>I can easily download my resume as PDF</soThat>
|
||||
<tasks>
|
||||
- Create ResumeDownloadButton component
|
||||
- Implement print mode detection
|
||||
- Add placeholder click handler
|
||||
- Integrate into resume page
|
||||
- Test button functionality
|
||||
</tasks>
|
||||
</story>
|
||||
|
||||
<acceptanceCriteria>
|
||||
<criterion id="AC1">Button is a floating action button (FAB) in bottom-right corner</criterion>
|
||||
<criterion id="AC2">Has download icon (i-heroicons-arrow-down-tray)</criterion>
|
||||
<criterion id="AC3">Has "Download PDF" text (or just icon on mobile)</criterion>
|
||||
<criterion id="AC4">Has blue background color</criterion>
|
||||
<criterion id="AC5">Has fixed position (doesn't scroll)</criterion>
|
||||
<criterion id="AC6">Has shadow for elevation</criterion>
|
||||
<criterion id="AC7">Has the `.no-print` class (hidden in PDF)</criterion>
|
||||
<criterion id="AC8">Button is not visible when `?print=true` parameter is present</criterion>
|
||||
</acceptanceCriteria>
|
||||
|
||||
<artifacts>
|
||||
<docs>
|
||||
<doc>
|
||||
<path>docs/architecture.md</path>
|
||||
<title>Resume Export Feature - Architecture Document</title>
|
||||
<section>Novel Pattern: WYSIWYG PDF Export</section>
|
||||
<snippet>Query Parameter: `?print=true` hides download button for PDF generation.</snippet>
|
||||
</doc>
|
||||
<doc>
|
||||
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
|
||||
<title>Epic Technical Specification: Resume Preview Page</title>
|
||||
<section>AC12: Download Button</section>
|
||||
<snippet>Floating action button in bottom-right corner with download icon, blue background, fixed position, shadow, and .no-print class.</snippet>
|
||||
</doc>
|
||||
</docs>
|
||||
<code>
|
||||
<artifact>
|
||||
<path>app/pages/resume.vue</path>
|
||||
<kind>page</kind>
|
||||
<symbol>resume page</symbol>
|
||||
<reason>Will import and render this button component, provides isPrintMode prop</reason>
|
||||
</artifact>
|
||||
</code>
|
||||
<dependencies>
|
||||
<node>
|
||||
<package name="@nuxt/ui" version="^4.0.x" />
|
||||
</node>
|
||||
</dependencies>
|
||||
</artifacts>
|
||||
|
||||
<constraints>
|
||||
- Use Nuxt UI UButton component
|
||||
- Icon: i-heroicons-arrow-down-tray
|
||||
- Position: fixed bottom-6 right-6
|
||||
- Color: primary (blue)
|
||||
- Size: lg
|
||||
- Shadow: shadow-lg
|
||||
- Z-index: z-50 (above all content)
|
||||
- Responsive: Hide text on mobile with "hidden sm:inline"
|
||||
- Print: .no-print class and v-if="!isPrintMode"
|
||||
- File location: app/components/resume/ResumeDownloadButton.vue
|
||||
- Click handler: Placeholder console.log (Epic 3 will implement actual PDF download)
|
||||
</constraints>
|
||||
|
||||
<interfaces>
|
||||
<interface>
|
||||
<name>ResumeDownloadButton Props</name>
|
||||
<kind>Vue component props</kind>
|
||||
<signature>interface Props { isPrintMode?: boolean }</signature>
|
||||
<path>app/components/resume/ResumeDownloadButton.vue</path>
|
||||
</interface>
|
||||
<interface>
|
||||
<name>UButton</name>
|
||||
<kind>Nuxt UI component</kind>
|
||||
<signature><UButton icon="..." size="..." color="..." class="..." @click="..." /></signature>
|
||||
<path>@nuxt/ui</path>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<tests>
|
||||
<standards>Vue component testing with Vitest. Test visibility, positioning, and click handler.</standards>
|
||||
<locations>app/components/**/*.spec.ts</locations>
|
||||
<ideas>
|
||||
<idea ac="AC1">Mount component, verify fixed positioning and bottom-right placement</idea>
|
||||
<idea ac="AC2">Check icon prop is set correctly</idea>
|
||||
<idea ac="AC4">Verify blue background color</idea>
|
||||
<idea ac="AC5">Check fixed position class</idea>
|
||||
<idea ac="AC6">Verify shadow class applied</idea>
|
||||
<idea ac="AC7">Check .no-print class exists</idea>
|
||||
<idea ac="AC8">Mount with isPrintMode=true, verify button not rendered</idea>
|
||||
</ideas>
|
||||
</tests>
|
||||
</story-context>
|
||||
@@ -0,0 +1,227 @@
|
||||
# Story 2.5: Create Download Button Component
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
As a user,
|
||||
I want a prominent download button,
|
||||
so that I can easily download my resume as PDF.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **AC1:** Given I'm on the resume page, when I see the download button, then it's a floating action button (FAB) in bottom-right corner
|
||||
2. **AC2:** It has download icon (i-heroicons-arrow-down-tray)
|
||||
3. **AC3:** It has "Download PDF" text (or just icon on mobile)
|
||||
4. **AC4:** It has blue background color
|
||||
5. **AC5:** It has fixed position (doesn't scroll)
|
||||
6. **AC6:** It has shadow for elevation
|
||||
7. **AC7:** It has the `.no-print` class (hidden in PDF)
|
||||
8. **AC8:** Given I'm on the page with `?print=true`, when the page renders, then the button is not visible
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Create ResumeDownloadButton component (AC: #1-#7)
|
||||
- [ ] Create `app/components/resume/ResumeDownloadButton.vue`
|
||||
- [ ] Use Nuxt UI `UButton` component
|
||||
- [ ] Set icon to `i-heroicons-arrow-down-tray`
|
||||
- [ ] Add "Download PDF" text
|
||||
- [ ] Apply blue background: `color="primary"` or `bg-blue-600`
|
||||
- [ ] Set fixed position: `fixed bottom-6 right-6`
|
||||
- [ ] Add shadow: `shadow-lg`
|
||||
- [ ] Add `.no-print` class
|
||||
|
||||
- [ ] Implement print mode detection (AC: #8)
|
||||
- [ ] Accept prop: `isPrintMode` (boolean)
|
||||
- [ ] Use `v-if="!isPrintMode"` to conditionally render
|
||||
- [ ] Ensure button hidden when `?print=true`
|
||||
|
||||
- [ ] Add placeholder click handler
|
||||
- [ ] Add `@click` event handler
|
||||
- [ ] For now, log to console: "Download PDF clicked"
|
||||
- [ ] Note: Actual PDF generation will be implemented in Epic 3
|
||||
|
||||
- [ ] Integrate into resume page (AC: #1-#8)
|
||||
- [ ] Import component in `pages/resume.vue`
|
||||
- [ ] Pass `isPrintMode` prop from route query
|
||||
- [ ] Place button outside ResumePreview container
|
||||
- [ ] Verify button appears in bottom-right corner
|
||||
|
||||
- [ ] Test button functionality
|
||||
- [ ] Verify button appears in bottom-right corner
|
||||
- [ ] Check fixed position (doesn't scroll with page)
|
||||
- [ ] Verify blue background and shadow
|
||||
- [ ] Test icon displays correctly
|
||||
- [ ] Check "Download PDF" text visibility
|
||||
- [ ] Test click handler (console log)
|
||||
- [ ] Verify button hidden with `?print=true`
|
||||
- [ ] Test print preview (Ctrl+P) - button should be hidden
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Alignment
|
||||
|
||||
**From Architecture Doc:**
|
||||
- File location: `app/components/resume/ResumeDownloadButton.vue`
|
||||
- Component: Use Nuxt UI `UButton` component
|
||||
- Icon: Heroicons arrow-down-tray
|
||||
- Position: Fixed bottom-right (FAB pattern)
|
||||
|
||||
**From Tech Spec Epic 2:**
|
||||
- AC12 maps to this story
|
||||
- Button hidden in print mode for PDF generation
|
||||
- Placeholder for Epic 3 integration
|
||||
|
||||
### Learnings from Previous Story
|
||||
|
||||
**From Story 2.4 (Status: drafted)**
|
||||
- Sidebar components created (Contact, Skills, Education, Languages)
|
||||
- All resume sections now complete
|
||||
- ResumePreview fully populated with content
|
||||
- **Integration Point**: Add download button to resume page, outside ResumePreview container
|
||||
|
||||
**From Story 2.1 (Status: drafted)**
|
||||
- Print mode detection implemented via `useRoute().query.print`
|
||||
- `isPrintMode` computed property available in `pages/resume.vue`
|
||||
- **Integration Point**: Pass `isPrintMode` to download button component
|
||||
|
||||
[Source: docs/sprint-artifacts/2-4-create-resume-sidebar-components.md]
|
||||
[Source: docs/sprint-artifacts/2-1-create-resume-page-route.md]
|
||||
|
||||
### Project Structure Notes
|
||||
|
||||
**File to Create:**
|
||||
- `app/components/resume/ResumeDownloadButton.vue`
|
||||
|
||||
**Files to Modify:**
|
||||
- `app/pages/resume.vue` - Import and place download button
|
||||
|
||||
**Dependencies:**
|
||||
- `@nuxt/ui` - `UButton` component
|
||||
- Heroicons icon set (via Nuxt UI)
|
||||
|
||||
**Future Integration:**
|
||||
- Epic 3 Story 3.2 will create `useResumePdf()` composable
|
||||
- Epic 3 Story 3.3 will connect this button to PDF generation
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
**ResumeDownloadButton.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
isPrintMode?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const handleDownload = () => {
|
||||
// Placeholder - will be replaced in Epic 3 Story 3.3
|
||||
console.log('Download PDF clicked')
|
||||
// Future: const { downloadPdf } = useResumePdf()
|
||||
// Future: await downloadPdf()
|
||||
}
|
||||
</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>
|
||||
|
||||
<style scoped>
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Integration in pages/resume.vue:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: false
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const isPrintMode = computed(() => route.query.print === 'true')
|
||||
|
||||
useHead({
|
||||
title: 'Resume - Ali Arghyani',
|
||||
meta: [
|
||||
{ name: 'robots', content: 'noindex' }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-white">
|
||||
<ResumePreview />
|
||||
<ResumeDownloadButton :is-print-mode="isPrintMode" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Responsive Behavior:**
|
||||
- Desktop: Shows "Download PDF" text with icon
|
||||
- Mobile: Shows icon only (text hidden with `hidden sm:inline`)
|
||||
|
||||
**Z-Index:**
|
||||
- Set `z-50` to ensure button appears above all content
|
||||
|
||||
**Testing Checklist:**
|
||||
- [ ] Button appears in bottom-right corner
|
||||
- [ ] Button has fixed position (stays in place when scrolling)
|
||||
- [ ] Blue background color applied
|
||||
- [ ] Shadow visible for elevation
|
||||
- [ ] Download icon displays correctly
|
||||
- [ ] "Download PDF" text visible on desktop
|
||||
- [ ] Text hidden on mobile (< 640px)
|
||||
- [ ] Click handler logs to console
|
||||
- [ ] Button hidden with `?print=true` parameter
|
||||
- [ ] Button hidden in print preview (Ctrl+P)
|
||||
- [ ] Button has proper z-index (appears above content)
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/architecture.md#Implementation-Patterns]
|
||||
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC12-Download-Button]
|
||||
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC13-Print-Mode]
|
||||
- [Source: docs/epics.md#Story-2.5-Create-Download-Button-Component]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Context Reference
|
||||
|
||||
- docs/sprint-artifacts/2-5-create-download-button-component.context.xml
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
<!-- Will be filled by dev agent -->
|
||||
|
||||
### Debug Log References
|
||||
|
||||
<!-- Will be filled by dev agent during implementation -->
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
<!-- Will be filled by dev agent after completion -->
|
||||
|
||||
### File List
|
||||
|
||||
<!-- Will be filled by dev agent with created/modified files -->
|
||||
|
||||
---
|
||||
|
||||
**Change Log:**
|
||||
- 2025-11-30: Story drafted by SM agent (mahdi)
|
||||
@@ -47,12 +47,12 @@ development_status:
|
||||
# Goal: Create pixel-perfect resume preview matching PDF output
|
||||
# FRs: FR5-9, FR15-25
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
epic-2: backlog
|
||||
2-1-create-resume-page-route: backlog
|
||||
2-2-create-resume-preview-container-component: backlog
|
||||
2-3-create-resume-header-main-content-components: backlog
|
||||
2-4-create-resume-sidebar-components: backlog
|
||||
2-5-create-download-button-component: backlog
|
||||
epic-2: contexted
|
||||
2-1-create-resume-page-route: ready-for-dev
|
||||
2-2-create-resume-preview-container-component: ready-for-dev
|
||||
2-3-create-resume-header-main-content-components: ready-for-dev
|
||||
2-4-create-resume-sidebar-components: ready-for-dev
|
||||
2-5-create-download-button-component: ready-for-dev
|
||||
epic-2-retrospective: optional
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
# Epic Technical Specification: Resume Preview Page
|
||||
|
||||
Date: 2025-11-30
|
||||
Author: mahdi
|
||||
Epic ID: 2
|
||||
Status: Draft
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Epic 2 delivers the Resume Preview Page - a pixel-perfect, standalone web page at `/resume` that renders the resume exactly as it will appear in the PDF export. This epic creates the visual foundation of the Resume Export feature, implementing all UI components with a two-column layout (sidebar + main content) using the Blue & White Clean Professional design template.
|
||||
|
||||
The preview page serves dual purposes: (1) user-facing preview for verification before download, and (2) source page for server-side PDF generation via Puppeteer. The WYSIWYG (What You See Is What You Get) approach ensures perfect consistency between web and PDF output.
|
||||
|
||||
## Objectives and Scope
|
||||
|
||||
**In Scope:**
|
||||
- Standalone `/resume` route with no site navigation
|
||||
- Two-column responsive layout (35% sidebar, 65% main content)
|
||||
- Six modular Vue components for resume sections
|
||||
- Blue (#2563eb) and white color scheme with Inter typography
|
||||
- Print-optimized CSS for PDF generation compatibility
|
||||
- A4 aspect ratio container (210mm × 297mm)
|
||||
- Floating download button (hidden in print mode via `?print=true`)
|
||||
- ATS-compatible HTML structure (semantic headings, no layout tables)
|
||||
|
||||
**Out of Scope:**
|
||||
- PDF generation logic (Epic 3)
|
||||
- Resume data creation (Epic 1 - already completed)
|
||||
- Persian language support (future)
|
||||
- Multiple template designs (future)
|
||||
- Customization UI (future)
|
||||
|
||||
## System Architecture Alignment
|
||||
|
||||
**Framework:** Nuxt 4.1.3 with Vue 3 Composition API
|
||||
**Styling:** Tailwind CSS 4.1.x with Nuxt UI 4.0.x components
|
||||
**Fonts:** Inter (via @nuxt/fonts) for English text
|
||||
**Icons:** Nuxt UI Icon component with Iconify icons
|
||||
|
||||
**Architecture Decisions Referenced:**
|
||||
- ADR-002: Vue Components as Templates - Each resume section is a standalone component
|
||||
- Novel Pattern: WYSIWYG PDF Export - Single component source for web and PDF
|
||||
- Naming Conventions: PascalCase for components, Tailwind utilities for styling
|
||||
|
||||
**Data Source:** `useResumeData()` composable (from Epic 1) provides reactive access to `app/data/resume.en.ts`
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### Services and Modules
|
||||
|
||||
| Component | Responsibility | Inputs | Outputs | Owner |
|
||||
|-----------|---------------|--------|---------|-------|
|
||||
| `pages/resume.vue` | Route handler, layout wrapper | Query param `?print` | Renders ResumePreview | Story 2.1 |
|
||||
| `ResumePreview.vue` | Container, two-column grid | Resume data | Full resume layout | Story 2.2 |
|
||||
| `ResumeHeader.vue` | Name and job title display | `basics.name`, `basics.label` | Header section | Story 2.3 |
|
||||
| `ResumeSummary.vue` | Professional summary | `basics.summary` | Summary section | Story 2.3 |
|
||||
| `ResumeExperience.vue` | Work history with highlights | `work[]` array | Experience section | Story 2.3 |
|
||||
| `ResumeContact.vue` | Contact info with icons | `basics` (email, phone, location, profiles) | Contact section | Story 2.4 |
|
||||
| `ResumeSkills.vue` | Technical skills by category | `skills[]` array | Skills section | Story 2.4 |
|
||||
| `ResumeEducation.vue` | Education history | `education[]` array | Education section | Story 2.4 |
|
||||
| `ResumeLanguages.vue` | Language proficiencies | `languages[]` array | Languages section | Story 2.4 |
|
||||
| `ResumeDownloadButton.vue` | Floating action button | `?print` query param | Download trigger | Story 2.5 |
|
||||
|
||||
### Data Models and Contracts
|
||||
|
||||
**Input:** All components consume data from `useResumeData()` composable:
|
||||
|
||||
```typescript
|
||||
const { resume, formatDate, getFullName, getPdfFilename } = useResumeData()
|
||||
```
|
||||
|
||||
**Resume Data Structure (from Epic 1):**
|
||||
- `resume.basics`: Name, label, email, phone, location, profiles, summary
|
||||
- `resume.work[]`: Company, position, startDate, endDate, highlights[]
|
||||
- `resume.education[]`: Institution, area, studyType, startDate, endDate
|
||||
- `resume.skills[]`: Name (category), keywords[]
|
||||
- `resume.languages[]`: Language, fluency
|
||||
|
||||
**Date Format:** YYYY-MM strings formatted to "Jan 2023" via `formatDate()` helper
|
||||
|
||||
### APIs and Interfaces
|
||||
|
||||
**Component Props:**
|
||||
|
||||
```typescript
|
||||
// ResumePreview.vue - No props (uses composable)
|
||||
// All child components receive data via props from parent
|
||||
|
||||
// ResumeHeader.vue
|
||||
interface Props {
|
||||
name: string
|
||||
label: string // Job title
|
||||
}
|
||||
|
||||
// ResumeExperience.vue
|
||||
interface Props {
|
||||
work: WorkExperience[]
|
||||
}
|
||||
|
||||
// ResumeContact.vue
|
||||
interface Props {
|
||||
basics: ResumeBasics
|
||||
}
|
||||
|
||||
// Similar pattern for other components
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `?print=true`: Hides download button, optimizes for PDF generation
|
||||
|
||||
### Workflows and Sequencing
|
||||
|
||||
**User Flow:**
|
||||
1. User navigates to `/resume`
|
||||
2. Page loads with `layout: false` (standalone)
|
||||
3. `ResumePreview.vue` fetches data via `useResumeData()`
|
||||
4. Two-column grid renders:
|
||||
- Left sidebar: Contact → Skills → Education → Languages
|
||||
- Right main: Header → Summary → Experience
|
||||
5. Floating download button appears (bottom-right)
|
||||
6. User clicks download → triggers Epic 3 PDF generation
|
||||
|
||||
**PDF Generation Flow (Epic 3 integration):**
|
||||
1. Puppeteer navigates to `/resume?print=true`
|
||||
2. Same components render without download button
|
||||
3. Puppeteer captures page as PDF
|
||||
4. Result: Pixel-perfect match to web preview
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### Performance
|
||||
|
||||
- **Page Load:** < 1 second (static data, no API calls)
|
||||
- **LCP (Largest Contentful Paint):** < 1.5s
|
||||
- **CLS (Cumulative Layout Shift):** 0 (fixed dimensions)
|
||||
- **Font Loading:** Inter font preloaded via @nuxt/fonts
|
||||
|
||||
**Strategy:** Inline critical CSS, use Tailwind JIT, no external API dependencies
|
||||
|
||||
### Security
|
||||
|
||||
- **No PII Exposure:** Resume data is static, no user input
|
||||
- **SEO:** `noindex` meta tag for privacy (resume not indexed by search engines)
|
||||
- **XSS Protection:** Vue's automatic escaping for all text content
|
||||
|
||||
### Reliability/Availability
|
||||
|
||||
- **Static Rendering:** Page works without JavaScript (SSR)
|
||||
- **Graceful Degradation:** Print styles work even if JS fails
|
||||
- **Error Handling:** Composable returns empty data if file missing (prevents crashes)
|
||||
|
||||
### Observability
|
||||
|
||||
- **Console Logging:** Development mode logs component mount/unmount
|
||||
- **Error Boundaries:** Vue error handlers catch component failures
|
||||
- **Performance Monitoring:** Nuxt DevTools tracks component render times
|
||||
|
||||
## Dependencies and Integrations
|
||||
|
||||
**External Dependencies:**
|
||||
- `@nuxt/fonts` (0.11.x): Inter font loading
|
||||
- `@nuxt/ui` (4.0.x): UButton, UIcon components
|
||||
- Tailwind CSS (4.1.x): Utility classes
|
||||
|
||||
**Internal Dependencies:**
|
||||
- `app/types/resume.ts`: TypeScript interfaces (Epic 1)
|
||||
- `app/data/resume.en.ts`: Resume data (Epic 1)
|
||||
- `app/composables/useResumeData.ts`: Data access composable (Epic 1)
|
||||
|
||||
**Integration Points:**
|
||||
- Epic 3: `/api/resume/pdf` will navigate to `/resume?print=true`
|
||||
- Future: Persian support will use `app/data/resume.fa.ts`
|
||||
|
||||
## Acceptance Criteria (Authoritative)
|
||||
|
||||
### AC1: Standalone Resume Route
|
||||
**Given** I navigate to `/resume`
|
||||
**When** the page loads
|
||||
**Then** I see the resume preview with no site header/footer
|
||||
**And** page title is "Resume - Ali Arghyani"
|
||||
**And** meta tag `<meta name="robots" content="noindex">` is present
|
||||
|
||||
### AC2: Two-Column Layout
|
||||
**Given** the resume page is rendered on desktop
|
||||
**When** I view the layout
|
||||
**Then** I see a two-column grid:
|
||||
- Left sidebar (35% width): Contact, Skills, Education, Languages
|
||||
- Right main content (65% width): Header, Summary, Experience
|
||||
**And** container has A4 aspect ratio (210mm × 297mm)
|
||||
**And** page margins are 24px (1.5rem)
|
||||
|
||||
### AC3: Responsive Behavior
|
||||
**Given** the resume page is rendered on mobile
|
||||
**When** viewport width < 768px
|
||||
**Then** layout switches to single column
|
||||
**And** sidebar sections appear above main content
|
||||
|
||||
### AC4: Color Scheme and Typography
|
||||
**Given** the resume is displayed
|
||||
**When** I inspect the styling
|
||||
**Then** primary color is blue (#2563eb) for headers and icons
|
||||
**And** background is white
|
||||
**And** text is dark gray (#1f2937)
|
||||
**And** font family is Inter
|
||||
**And** body text size is 14px (0.875rem)
|
||||
|
||||
### AC5: Header Section
|
||||
**Given** the header component renders
|
||||
**When** I view the top of the resume
|
||||
**Then** I see full name in large bold text (2rem)
|
||||
**And** job title below name (1.25rem, gray)
|
||||
**And** blue accent line or background element
|
||||
|
||||
### AC6: Contact Section
|
||||
**Given** the contact component renders
|
||||
**When** I view the sidebar
|
||||
**Then** I see email with icon (i-mdi-email)
|
||||
**And** phone with icon (i-mdi-phone)
|
||||
**And** location with icon (i-mdi-map-marker)
|
||||
**And** social profiles (LinkedIn, GitHub) with icons
|
||||
**And** all links are clickable (mailto:, tel:, https://)
|
||||
|
||||
### AC7: Skills Section
|
||||
**Given** the skills component renders
|
||||
**When** I view the sidebar
|
||||
**Then** I see "Skills" section header
|
||||
**And** skill categories as subheaders
|
||||
**And** keywords displayed as tags or comma-separated list
|
||||
|
||||
### AC8: Education Section
|
||||
**Given** the education component renders
|
||||
**When** I view the sidebar
|
||||
**Then** I see degree type and field (e.g., "B.Sc. Computer Science")
|
||||
**And** institution name
|
||||
**And** date range formatted (e.g., "2018 - 2022")
|
||||
|
||||
### AC9: Languages Section
|
||||
**Given** the languages component renders
|
||||
**When** I view the sidebar
|
||||
**Then** I see "Languages" section header
|
||||
**And** each language with fluency level (e.g., "English - Fluent")
|
||||
|
||||
### AC10: Summary Section
|
||||
**Given** the summary component renders
|
||||
**When** I view the main content
|
||||
**Then** I see "Profile" or "Summary" section header
|
||||
**And** professional summary paragraph
|
||||
**And** line height is 1.6 for readability
|
||||
|
||||
### AC11: Experience Section
|
||||
**Given** the experience component renders
|
||||
**When** I view the main content
|
||||
**Then** I see for each job:
|
||||
- Position title (bold)
|
||||
- Company name
|
||||
- Date range (formatted: "Jan 2022 - Present")
|
||||
- Bullet points for highlights (• character)
|
||||
**And** jobs are sorted by date (most recent first)
|
||||
**And** "Present" is shown for current jobs (no endDate)
|
||||
|
||||
### AC12: Download Button
|
||||
**Given** I'm on the resume page
|
||||
**When** I view the page
|
||||
**Then** I see a floating action button in bottom-right corner
|
||||
**And** it has download icon (i-heroicons-arrow-down-tray)
|
||||
**And** it has "Download PDF" text
|
||||
**And** it has blue background color
|
||||
**And** it has fixed position (doesn't scroll)
|
||||
**And** it has shadow for elevation
|
||||
**And** it has `.no-print` class
|
||||
|
||||
### AC13: Print Mode
|
||||
**Given** I navigate to `/resume?print=true`
|
||||
**When** the page loads
|
||||
**Then** the download button is not visible
|
||||
**And** all other content renders normally
|
||||
|
||||
### AC14: Print Styles
|
||||
**Given** the page is printed or captured by Puppeteer
|
||||
**When** print media query is active
|
||||
**Then** `.no-print` elements are hidden
|
||||
**And** colors print correctly (printBackground: true)
|
||||
**And** page breaks are controlled
|
||||
**And** A4 dimensions are maintained
|
||||
|
||||
### AC15: ATS Compatibility
|
||||
**Given** the HTML structure is inspected
|
||||
**When** I check semantic elements
|
||||
**Then** name uses `<h1>` tag
|
||||
**And** section headers use `<h2>` tags
|
||||
**And** no layout tables are used (CSS Grid/Flexbox only)
|
||||
**And** all text is real HTML text (not images)
|
||||
|
||||
## Traceability Mapping
|
||||
|
||||
| AC | Spec Section | Components | Test Idea |
|
||||
|----|--------------|------------|-----------|
|
||||
| AC1 | Standalone Route | `pages/resume.vue` | Navigate to /resume, verify no nav, check meta tags |
|
||||
| AC2 | Two-Column Layout | `ResumePreview.vue` | Inspect grid, measure column widths, verify A4 ratio |
|
||||
| AC3 | Responsive | `ResumePreview.vue` | Resize viewport, verify single column on mobile |
|
||||
| AC4 | Color/Typography | All components | Inspect computed styles, verify colors and fonts |
|
||||
| AC5 | Header | `ResumeHeader.vue` | Check h1 text, font size, job title styling |
|
||||
| AC6 | Contact | `ResumeContact.vue` | Verify icons, links, click mailto/tel links |
|
||||
| AC7 | Skills | `ResumeSkills.vue` | Check section header, categories, keywords |
|
||||
| AC8 | Education | `ResumeEducation.vue` | Verify degree, institution, date formatting |
|
||||
| AC9 | Languages | `ResumeLanguages.vue` | Check language list, fluency levels |
|
||||
| AC10 | Summary | `ResumeSummary.vue` | Verify section header, paragraph, line height |
|
||||
| AC11 | Experience | `ResumeExperience.vue` | Check job entries, date formatting, bullet points |
|
||||
| AC12 | Download Button | `ResumeDownloadButton.vue` | Verify FAB position, icon, styling, shadow |
|
||||
| AC13 | Print Mode | `pages/resume.vue` | Add ?print=true, verify button hidden |
|
||||
| AC14 | Print Styles | CSS | Trigger print preview, verify styles |
|
||||
| AC15 | ATS Compatibility | HTML structure | Inspect DOM, verify semantic tags, no tables |
|
||||
|
||||
## Risks, Assumptions, Open Questions
|
||||
|
||||
**Risks:**
|
||||
- **Risk:** Font loading delay causes layout shift
|
||||
- **Mitigation:** Use @nuxt/fonts with preload, font-display: swap
|
||||
- **Risk:** Print styles differ across browsers
|
||||
- **Mitigation:** Test in Chrome, Firefox, Safari; use Puppeteer for PDF (consistent)
|
||||
- **Risk:** Long content causes page overflow
|
||||
- **Mitigation:** Test with max content length, add overflow handling
|
||||
|
||||
**Assumptions:**
|
||||
- **Assumption:** Inter font is ATS-compatible
|
||||
- **Validation:** Inter is a standard web font, widely supported
|
||||
- **Assumption:** Two-column layout works for all content lengths
|
||||
- **Validation:** Test with varying experience entries (2-5 jobs)
|
||||
- **Assumption:** Blue (#2563eb) has sufficient contrast
|
||||
- **Validation:** WCAG AA contrast ratio verified (4.5:1 minimum)
|
||||
|
||||
**Open Questions:**
|
||||
- **Question:** Should mobile view show download button?
|
||||
- **Answer:** Yes, but consider smaller size or icon-only
|
||||
- **Question:** How to handle very long job titles or company names?
|
||||
- **Answer:** Use text truncation with ellipsis, test with max lengths
|
||||
- **Question:** Should we add a "Print" button in addition to "Download PDF"?
|
||||
- **Answer:** No for MVP, browser print (Ctrl+P) is sufficient
|
||||
|
||||
## Test Strategy Summary
|
||||
|
||||
**Unit Tests:**
|
||||
- Each component renders with sample data
|
||||
- Date formatting helper works correctly
|
||||
- Composable returns expected data structure
|
||||
|
||||
**Integration Tests:**
|
||||
- Full page renders with all components
|
||||
- Print mode hides download button
|
||||
- Responsive layout switches at breakpoint
|
||||
|
||||
**Visual Regression Tests:**
|
||||
- Screenshot comparison: web preview vs expected design
|
||||
- Print preview matches web preview
|
||||
- Mobile layout matches design
|
||||
|
||||
**Manual Tests:**
|
||||
- Navigate to /resume, verify all sections
|
||||
- Click all links (email, phone, social profiles)
|
||||
- Test print preview (Ctrl+P)
|
||||
- Test on mobile device
|
||||
- Verify ATS compatibility (copy/paste text from PDF)
|
||||
|
||||
**Performance Tests:**
|
||||
- Measure page load time (< 1s target)
|
||||
- Check LCP and CLS metrics
|
||||
- Verify font loading doesn't block render
|
||||
|
||||
**Acceptance Tests:**
|
||||
- Run through all 15 ACs with real data
|
||||
- Verify pixel-perfect match to design template
|
||||
- Confirm Epic 3 can generate PDF from this page
|
||||
|
||||
---
|
||||
|
||||
**Epic 2 Tech Spec Complete**
|
||||
**Status:** Ready for Story Creation
|
||||
**Next Step:** SM creates Story 2.1 draft
|
||||
Reference in New Issue
Block a user