mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-16 21:14:31 +03:30
feat(resume): enhance print styling and refactor component structure
- Add print media queries to main.css for proper PDF export formatting - Ensure white background and light color scheme when printing - Refactor ResumeAdditionalInfo component to display skills in categorized format - Remove languages and certifications from additional info section - Add ResumeLanguages component for dedicated language display - Optimize spacing with print-specific margin utilities across all resume sections - Improve text wrapping and hyphenation in ResumeExperience component - Enhance ResumeHeader layout with print-optimized image sizing - Update resume data structure to support new component organization - Add research documentation on mentioning AI skills in resume - Update PDF export endpoint to work with refactored component structure - Remove outdated chat history file
This commit is contained in:
@@ -1,54 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { Skill, Language, Certification } from '~/types/resume'
|
||||
import type { Skill } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
skills: Skill[]
|
||||
languages?: Language[]
|
||||
certifications?: Certification[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Get only key technical skills (first 3 categories)
|
||||
const keySkills = computed(() => {
|
||||
return props.skills.slice(0, 3)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<section class="mb-6 print:mb-4">
|
||||
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
|
||||
Additional Information
|
||||
Skills & Qualifications
|
||||
</h2>
|
||||
|
||||
<!-- Key Technical Skills (simplified) -->
|
||||
<div v-if="keySkills?.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 keySkills" :key="skill.name">
|
||||
{{ skill.keywords.join(', ') }}<template v-if="idx < keySkills.length - 1">; </template>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Languages -->
|
||||
<div v-if="languages?.length" class="mb-2">
|
||||
<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.issuer }} - {{ cert.summary }}<template v-if="idx < certifications.length - 1">, </template>
|
||||
</template>
|
||||
</span>
|
||||
<!-- Technical Skills (categorized) -->
|
||||
<div v-if="skills?.length">
|
||||
<div class="space-y-0.5">
|
||||
<div v-for="skill in skills" :key="skill.name" class="text-sm">
|
||||
<span class="font-medium text-gray-800">{{ skill.name }}:</span>
|
||||
<span class="text-gray-700"> {{ skill.keywords.join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ function formatDateRange(start: string, end?: string): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mb-6">
|
||||
<section class="mb-6 print:mb-4">
|
||||
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
|
||||
Education
|
||||
</h2>
|
||||
|
||||
@@ -24,27 +24,40 @@ function formatDateRange(start: string, end?: string): string {
|
||||
</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">
|
||||
<section class="mb-6 print:mb-3">
|
||||
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-2 print:mb-1.5">
|
||||
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>
|
||||
<div v-for="job in sortedWork" :key="job.company + job.startDate" class="mb-4 last:mb-0 print:mb-3">
|
||||
<div class="flex justify-between items-start gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-semibold text-gray-800 break-words hyphens-none">{{ job.position }}</h3>
|
||||
<p class="text-sm text-gray-600">{{ job.company }}</p>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500 whitespace-nowrap">
|
||||
<span class="text-sm text-gray-500 whitespace-nowrap flex-shrink-0">
|
||||
{{ formatDateRange(job.startDate, job.endDate) }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1">
|
||||
<ul class="mt-2 space-y-1 print:mt-1 print:space-y-0.5">
|
||||
<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">
|
||||
class="text-sm text-gray-700 pl-4 relative before:content-['•'] before:absolute before:left-0 break-words hyphens-none">
|
||||
{{ highlight }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hyphens-none {
|
||||
hyphens: none;
|
||||
-webkit-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
}
|
||||
|
||||
.break-words {
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,23 +9,61 @@ defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-6 mb-6">
|
||||
<div class="flex gap-6 mb-6 print:mb-2 print:pb-0">
|
||||
<!-- 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 " />
|
||||
<img :src="basics.image" :alt="basics.name" class="w-32 h-32 object-cover rounded-full print:w-24 print:h-24" />
|
||||
</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>
|
||||
<h1 class="text-3xl font-bold text-gray-900 print:text-xl print:mb-0.5">{{ basics.name }}</h1>
|
||||
<p class="text-xl text-gray-600 mt-1 print:text-base print:mt-0">{{ 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 class="mt-3 flex flex-wrap gap-x-4 gap-y-1.5 text-sm text-gray-700 print:mt-1.5 print:text-xs print:gap-x-3">
|
||||
<!-- Location -->
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<UIcon name="i-heroicons-map-pin" class="text-gray-500 flex-shrink-0" />
|
||||
{{ basics.location.city }}, {{ basics.location.country }}
|
||||
</span>
|
||||
|
||||
<!-- Phone -->
|
||||
<a :href="`tel:${basics.phone}`" class="inline-flex items-center gap-1.5 hover:text-blue-600 transition-colors">
|
||||
<UIcon name="i-heroicons-phone" class="text-gray-500 flex-shrink-0" />
|
||||
{{ basics.phone }}
|
||||
</a>
|
||||
|
||||
<!-- Email -->
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<UIcon name="i-heroicons-envelope" class="text-gray-500 flex-shrink-0" />
|
||||
<a :href="`mailto:${basics.email}`" class="hover:text-blue-600 transition-colors">
|
||||
{{ basics.email }}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- LinkedIn -->
|
||||
<a v-if="basics.profiles?.find(p => p.network === 'LinkedIn')"
|
||||
:href="basics.profiles.find(p => p.network === 'LinkedIn')!.url" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 hover:text-blue-600 transition-colors">
|
||||
<UIcon name="i-mdi-linkedin" class="text-gray-500 flex-shrink-0" />
|
||||
LinkedIn
|
||||
</a>
|
||||
|
||||
<!-- Website (Portfolio) -->
|
||||
<a v-if="basics.profiles?.find(p => p.network === 'Portfolio')"
|
||||
:href="basics.profiles.find(p => p.network === 'Portfolio')!.url" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 hover:text-blue-600 transition-colors">
|
||||
<UIcon name="i-heroicons-globe-alt" class="text-gray-500 flex-shrink-0" />
|
||||
Website
|
||||
</a>
|
||||
|
||||
<!-- GitHub -->
|
||||
<a v-if="basics.profiles?.find(p => p.network === 'GitHub')"
|
||||
:href="basics.profiles.find(p => p.network === 'GitHub')!.url" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 hover:text-blue-600 transition-colors">
|
||||
<UIcon name="i-mdi-github" class="text-gray-500 flex-shrink-0" />
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { Language, Certification } from '~/types/resume'
|
||||
|
||||
interface Props {
|
||||
languages?: Language[]
|
||||
certifications?: Certification[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mb-6 print:mb-4">
|
||||
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-3">
|
||||
Languages & Certifications
|
||||
</h2>
|
||||
|
||||
<!-- Languages -->
|
||||
<div v-if="languages?.length" class="mb-2">
|
||||
<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.issuer }} - {{ cert.summary }}<template v-if="idx < certifications.length - 1">, </template>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -3,46 +3,79 @@ const { resume } = useResumeData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4 print:p-0 print:bg-white">
|
||||
<div class="resume-wrapper">
|
||||
<!-- A4 Container -->
|
||||
<div
|
||||
class="resume-container bg-white shadow-lg max-w-[210mm] min-h-[297mm] w-full print:shadow-none print:max-w-none">
|
||||
<div class="resume-container">
|
||||
<!-- Single-column vertical stack -->
|
||||
<div class="flex flex-col p-6">
|
||||
<div class="resume-content">
|
||||
<!-- Header with Photo + Name + Contact -->
|
||||
<ResumeHeader :basics="resume.basics" />
|
||||
|
||||
<!-- Summary -->
|
||||
<ResumeSummary :summary="resume.basics.summary" />
|
||||
|
||||
<!-- Skills & Qualifications -->
|
||||
<ResumeAdditionalInfo :skills="resume.skills" />
|
||||
|
||||
<!-- Experience -->
|
||||
<ResumeExperience :work="resume.work" />
|
||||
|
||||
<!-- Education -->
|
||||
<ResumeEducation :education="resume.education" />
|
||||
|
||||
<!-- Additional Information -->
|
||||
<ResumeAdditionalInfo :skills="resume.skills" :languages="resume.languages"
|
||||
:certifications="resume.certifications" />
|
||||
<!-- Languages & Certifications -->
|
||||
<ResumeLanguages :languages="resume.languages" :certifications="resume.certificates" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.resume-wrapper {
|
||||
background: #f3f4f6;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.resume-container {
|
||||
background: white;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
max-width: 210mm;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.resume-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
.resume-wrapper {
|
||||
background: white !important;
|
||||
padding: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.resume-container {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
width: 210mm !important;
|
||||
max-width: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.resume-content {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
* {
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
hyphens: none !important;
|
||||
-webkit-hyphens: none !important;
|
||||
}
|
||||
|
||||
section {
|
||||
|
||||
@@ -7,10 +7,10 @@ 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">
|
||||
<section class="mb-6 print:mb-3">
|
||||
<h2 class="text-base font-bold text-blue-600 uppercase border-b-2 border-blue-600 pb-1 mb-2 print:mb-1.5">
|
||||
Summary
|
||||
</h2>
|
||||
<p class="text-sm text-gray-800 leading-relaxed">{{ summary }}</p>
|
||||
<p class="text-sm text-gray-800 leading-relaxed print:leading-normal">{{ summary }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user