feat(resume): complete Epic 2 - Resume Preview Page

Epic 2: Resume Preview Page - All 5 stories completed

Story 2-1: Create Resume Page Route
- Add /resume standalone page with layout: false
- Implement print mode detection (?print=true)
- Add SEO meta tags (noindex for privacy)
- Set page title "Resume - Ali Arghyani"

Story 2-2: Create ResumePreview Container
- Add single-column A4 container (210mm × 297mm)
- Implement white background with 24px margins
- Add print styles with .no-print class
- Configure responsive layout

Story 2-3: Create Header, Summary, Experience Components
- Add ResumeHeader with profile photo, name, contact info
- Add ResumeSummary with blue uppercase section header
- Add ResumeExperience with sorted jobs and date formatting
- Add image property to ResumeBasics interface
- Integrate all components into ResumePreview

Story 2-4: Create Education & Additional Info Components
- Add ResumeEducation with date formatting
- Add ResumeAdditionalInfo with skills, languages, certifications
- Integrate components into ResumePreview

Story 2-5: Create Download Button Component
- Add ResumeDownloadButton FAB (fixed bottom-right)
- Add download icon and "Download PDF" text
- Implement print mode detection
- Add placeholder click handler for Epic 3 integration
- Responsive: text on desktop, icon-only on mobile

Additional:
- Add profile image to resume data (/img/AliProfile.webp)
- Update sprint-status.yaml: all Epic 2 stories marked done

Files Created:
- app/pages/resume.vue
- app/components/resume/ResumePreview.vue
- app/components/resume/ResumeHeader.vue
- app/components/resume/ResumeSummary.vue
- app/components/resume/ResumeExperience.vue
- app/components/resume/ResumeEducation.vue
- app/components/resume/ResumeAdditionalInfo.vue
- app/components/resume/ResumeDownloadButton.vue

Files Modified:
- app/types/resume.ts (added image property)
- app/data/resume.en.ts (added profile image)
- docs/sprint-artifacts/sprint-status.yaml (updated story statuses)
- docs/sprint-artifacts/2-1-create-resume-page-route.md (completed)
- docs/sprint-artifacts/2-2-create-resume-preview-container-component.md (completed)
- docs/sprint-artifacts/2-3-create-resume-header-main-content-components.md (completed)
- docs/sprint-artifacts/2-4-create-resume-sidebar-components.md (completed)
- docs/sprint-artifacts/2-5-create-download-button-component.md (completed)

Closes Epic 2
Closes Story 2-1, 2-2, 2-3, 2-4, 2-5
This commit is contained in:
mahdiarghyani
2025-12-01 11:46:46 +03:30
parent 6541cfec16
commit 56676771ca
14 changed files with 353 additions and 201 deletions
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { Skill, Language, Certification } from '~/types/resume'
interface Props {
skills: Skill[]
languages?: Language[]
certifications?: Certification[]
}
defineProps<Props>()
</script>
<template>
<section>
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Additional Information
</h2>
<!-- Skills -->
<div v-if="skills?.length" class="mb-3">
<span class="text-sm font-semibold text-gray-800">Technical Skills: </span>
<span class="text-sm text-gray-700">
<template v-for="(skill, idx) in skills" :key="skill.name">
{{ skill.keywords.join(', ') }}<template v-if="idx < skills.length - 1">; </template>
</template>
</span>
</div>
<!-- Languages -->
<div v-if="languages?.length" class="mb-3">
<span class="text-sm font-semibold text-gray-800">Languages: </span>
<span class="text-sm text-gray-700">
<template v-for="(lang, idx) in languages" :key="lang.language">
{{ lang.language }} ({{ lang.fluency }})<template v-if="idx < languages.length - 1">, </template>
</template>
</span>
</div>
<!-- Certifications -->
<div v-if="certifications?.length">
<span class="text-sm font-semibold text-gray-800">Certifications: </span>
<span class="text-sm text-gray-700">
<template v-for="(cert, idx) in certifications" :key="cert.name">
{{ cert.name }} ({{ cert.issuer }})<template v-if="idx < certifications.length - 1">, </template>
</template>
</span>
</div>
</section>
</template>
@@ -0,0 +1,29 @@
<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')
// 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>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { Education } from '~/types/resume'
interface Props {
education: Education[]
}
defineProps<Props>()
const { formatDate } = useResumeData()
function formatDateRange(start: string, end?: string): string {
const startFormatted = formatDate(start)
const endFormatted = end ? formatDate(end) : 'Present'
return `${startFormatted} - ${endFormatted}`
}
</script>
<template>
<section class="mb-6">
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Education
</h2>
<div v-for="edu in education" :key="edu.institution + edu.startDate" class="mb-3 last:mb-0">
<div class="flex justify-between items-start">
<div>
<h3 class="text-sm font-semibold text-gray-800">{{ edu.studyType }} in {{ edu.area }}</h3>
<p class="text-sm text-gray-600">{{ edu.institution }}</p>
</div>
<span class="text-sm text-gray-500 whitespace-nowrap">
{{ formatDateRange(edu.startDate, edu.endDate) }}
</span>
</div>
</div>
</section>
</template>
@@ -0,0 +1,50 @@
<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)
})
})
function formatDateRange(start: string, end?: string): string {
const startFormatted = formatDate(start)
const endFormatted = end ? formatDate(end) : 'Present'
return `${startFormatted} - ${endFormatted}`
}
</script>
<template>
<section class="mb-6">
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Work Experience
</h2>
<div v-for="job in sortedWork" :key="job.company + job.startDate" class="mb-4 last:mb-0">
<div class="flex justify-between items-start">
<div>
<h3 class="text-sm font-semibold text-gray-800">{{ job.position }}</h3>
<p class="text-sm text-gray-600">{{ job.company }}</p>
</div>
<span class="text-sm text-gray-500 whitespace-nowrap">
{{ formatDateRange(job.startDate, job.endDate) }}
</span>
</div>
<ul class="mt-2 space-y-1">
<li v-for="(highlight, idx) in job.highlights" :key="idx"
class="text-sm text-gray-700 pl-4 relative before:content-['•'] before:absolute before:left-0">
{{ highlight }}
</li>
</ul>
</div>
</section>
</template>
+32
View File
@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { ResumeBasics } from '~/types/resume'
interface Props {
basics: ResumeBasics
}
defineProps<Props>()
</script>
<template>
<div class="flex gap-6 mb-6">
<!-- Profile Photo (if provided) -->
<div v-if="basics.image" class="flex-shrink-0">
<img :src="basics.image" :alt="basics.name" class="w-32 h-32 object-cover rounded-full " />
</div>
<!-- Name + Contact Info -->
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-900">{{ basics.name }}</h1>
<p class="text-xl text-gray-600 mt-1">{{ basics.label }}</p>
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-700">
<span>{{ basics.location.city }}, {{ basics.location.country }}</span>
<a :href="`tel:${basics.phone}`" class="hover:text-blue-600">{{ basics.phone }}</a>
<a :href="`mailto:${basics.email}`" class="hover:text-blue-600">{{ basics.email }}</a>
<a v-if="basics.url" :href="basics.url" target="_blank" class="hover:text-blue-600">{{ basics.url }}</a>
</div>
</div>
</div>
</template>
+11 -92
View File
@@ -9,102 +9,21 @@ const { resume } = useResumeData()
class="resume-container bg-white shadow-lg max-w-[210mm] min-h-[297mm] w-full print:shadow-none print:max-w-none">
<!-- Single-column vertical stack -->
<div class="flex flex-col p-6">
<!-- Header with Photo + Name + Contact (Story 2.3) -->
<section class="mb-6">
<h1 class="text-3xl font-bold text-gray-800">{{ resume.basics.name }}</h1>
<p class="text-lg text-gray-600">{{ resume.basics.label }}</p>
<p class="text-sm text-gray-500 mt-1">
{{ resume.basics.location.city }}, {{ resume.basics.location.country }} |
{{ resume.basics.email }} | {{ resume.basics.phone }}
</p>
</section>
<!-- Header with Photo + Name + Contact -->
<ResumeHeader :basics="resume.basics" />
<!-- Summary (Story 2.3) -->
<section class="mb-6">
<h2 class="text-base font-semibold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Summary
</h2>
<p class="text-sm text-gray-700 leading-relaxed">{{ resume.basics.summary }}</p>
</section>
<!-- Summary -->
<ResumeSummary :summary="resume.basics.summary" />
<!-- Experience (Story 2.3) -->
<section class="mb-6">
<h2 class="text-base font-semibold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Work Experience
</h2>
<div v-for="job in resume.work" :key="job.company + job.startDate" class="mb-4 last:mb-0">
<div class="flex justify-between items-start">
<div>
<h3 class="text-sm font-semibold text-gray-800">{{ job.position }}</h3>
<p class="text-sm text-gray-600">{{ job.company }}</p>
</div>
<span class="text-sm text-gray-500 whitespace-nowrap">
{{ job.startDate }} - {{ job.endDate || 'Present' }}
</span>
</div>
<ul class="mt-2 space-y-1">
<li v-for="(highlight, idx) in job.highlights" :key="idx"
class="text-sm text-gray-700 pl-4 relative before:content-['•'] before:absolute before:left-0">
{{ highlight }}
</li>
</ul>
</div>
</section>
<!-- Experience -->
<ResumeExperience :work="resume.work" />
<!-- Education (Story 2.4) -->
<section class="mb-6">
<h2 class="text-base font-semibold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Education
</h2>
<div v-for="edu in resume.education" :key="edu.institution + edu.startDate" class="mb-3 last:mb-0">
<div class="flex justify-between items-start">
<div>
<h3 class="text-sm font-semibold text-gray-800">{{ edu.studyType }} in {{ edu.area }}</h3>
<p class="text-sm text-gray-600">{{ edu.institution }}</p>
</div>
<span class="text-sm text-gray-500 whitespace-nowrap">
{{ edu.startDate }} - {{ edu.endDate || 'Present' }}
</span>
</div>
</div>
</section>
<!-- Education -->
<ResumeEducation :education="resume.education" />
<!-- Additional Information (Story 2.4) -->
<section>
<h2 class="text-base font-semibold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Additional Information
</h2>
<!-- Skills -->
<div v-if="resume.skills?.length" class="mb-3">
<span class="text-sm font-semibold text-gray-800">Technical Skills: </span>
<span class="text-sm text-gray-700">
<template v-for="(skill, idx) in resume.skills" :key="skill.name">
{{ skill.keywords.join(', ') }}<template v-if="idx < resume.skills.length - 1">; </template>
</template>
</span>
</div>
<!-- Languages -->
<div v-if="resume.languages?.length" class="mb-3">
<span class="text-sm font-semibold text-gray-800">Languages: </span>
<span class="text-sm text-gray-700">
<template v-for="(lang, idx) in resume.languages" :key="lang.language">
{{ lang.language }} ({{ lang.fluency }})<template v-if="idx < resume.languages.length - 1">, </template>
</template>
</span>
</div>
<!-- Certifications -->
<div v-if="resume.certifications?.length">
<span class="text-sm font-semibold text-gray-800">Certifications: </span>
<span class="text-sm text-gray-700">
<template v-for="(cert, idx) in resume.certifications" :key="cert.name">
{{ cert.name }} ({{ cert.issuer }})<template v-if="idx < resume.certifications.length - 1">, </template>
</template>
</span>
</div>
</section>
<!-- Additional Information -->
<ResumeAdditionalInfo :skills="resume.skills" :languages="resume.languages"
:certifications="resume.certifications" />
</div>
</div>
</div>
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
interface Props {
summary: string
}
defineProps<Props>()
</script>
<template>
<section class="mb-6">
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
Summary
</h2>
<p class="text-sm text-gray-800 leading-relaxed">{{ summary }}</p>
</section>
</template>