CV page improvements #22

Merged
robin merged 4 commits from dev into main 2026-09-05 20:38:55 +02:00
2 changed files with 184 additions and 59 deletions
Showing only changes of commit ff1e078be9 - Show all commits
+5 -2
View File
@@ -8,6 +8,7 @@ type IconName =
| 'linkedin'
| 'github'
| 'code'
| 'verified'
| 'summary'
| 'experience'
| 'education'
@@ -20,8 +21,8 @@ type IconName =
const props = defineProps<{ name: IconName }>()
// Sourced from lucide-static (ISC license): phone, mail, map-pin, linkedin,
// github, file-text, briefcase, graduation-cap, award, languages,
// chart-column, layers, heart.
// github, code, badge-check, file-text, briefcase, graduation-cap, award,
// languages, chart-column, layers, heart.
const MARKUP: Record<IconName, string> = {
phone:
'<path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384" />',
@@ -34,6 +35,8 @@ const MARKUP: Record<IconName, string> = {
github:
'<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" /><path d="M9 18c-4.51 2-5-2-7-2" />',
code: '<path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" />',
verified:
'<path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" /><path d="m16 9-5.5 5.5L8 12" />',
summary:
'<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" />',
experience:
+179 -57
View File
@@ -20,6 +20,13 @@ onBeforeUnmount(() => {
robotsMeta?.remove()
})
interface Project {
title: string
repo: string
desc: string
url: string
}
interface ExperienceEntry {
company: string
location: string
@@ -56,16 +63,24 @@ interface AdditionalExperience {
desc: string
}
interface Project {
title: string
repo: string
desc: string
url: string
}
const SKILL_MAX = 8
const LANGUAGE_MAX = 5
const projects: Project[] = [
{
title: 'Frontend',
repo: 'dttmr-fe',
desc: 'Offline-first PWA for shared, syncable to-do lists and tracking workouts. Vue 3 + TypeScript.',
url: 'https://git.dittmar.dev/robin/dttmr-fe',
},
{
title: 'API',
repo: 'dttmr-api',
desc: 'Go backend powering auth and sync for the to-do list/workout tracking app.',
url: 'https://git.dittmar.dev/robin/dttmr-api',
},
]
const experience: ExperienceEntry[] = [
{
company: 'Greenbone AG',
@@ -211,21 +226,6 @@ const skills: Skill[] = [
{ name: 'Claude Code', rating: 4 },
]
const projects: Project[] = [
{
title: 'Frontend',
repo: 'dttmr-fe',
desc: 'Offline-first PWA for shared, syncable to-do lists. Vue 3 + TypeScript.',
url: 'https://git.dittmar.dev/robin/dttmr-fe',
},
{
title: 'API',
repo: 'dttmr-api',
desc: 'Go backend powering auth and sync for the to-do list app.',
url: 'https://git.dittmar.dev/robin/dttmr-api',
},
]
const additionalExperiences: AdditionalExperience[] = [
{
title: 'Minecraft Mod',
@@ -252,19 +252,20 @@ const additionalExperiences: AdditionalExperience[] = [
<img class="avatar" :src="cvPhoto" alt="Portrait of Robin Dittmar" />
<div>
<h1>Robin Dittmar</h1>
<span class="tag tag-accent">Software Developer</span>
<span class="role-badge">Software Developer</span>
</div>
</div>
<div class="ledger-rule" aria-hidden="true"></div>
<section class="card info-card">
<section class="card info-card contact-card">
<h4>Contacts</h4>
<p class="row">
<span class="row-label"><CvIcon name="phone" />Phone</span>
<strong class="mono-num">+49159/01668799</strong>
<a class="mono-num" href="tel:+4915901668799">+49 1590 1668799</a>
</p>
<p class="row">
<span class="row-label"><CvIcon name="email" />Email</span>
<strong>robindittmar@gmail.com</strong>
<a href="mailto:robindittmar@gmail.com">robindittmar@gmail.com</a>
</p>
<p class="row">
<span class="row-label"><CvIcon name="location" />Location</span>
@@ -310,7 +311,7 @@ const additionalExperiences: AdditionalExperience[] = [
</section>
<h1 class="section-heading"><CvIcon name="experience" />Experience</h1>
<ul class="cards">
<ul class="cards timeline timeline-current">
<li v-for="job in experience" :key="job.company + job.dates">
<article class="card job-card">
<div class="job-card-head">
@@ -327,7 +328,7 @@ const additionalExperiences: AdditionalExperience[] = [
</ul>
<h1 class="section-heading"><CvIcon name="education" />Education</h1>
<ul class="cards">
<ul class="cards timeline">
<li v-for="edu in education" :key="edu.school">
<article class="card job-card">
<div class="job-card-head">
@@ -342,8 +343,11 @@ const additionalExperiences: AdditionalExperience[] = [
<h1 class="section-heading"><CvIcon name="achievements" />Key Achievements</h1>
<section class="card info-card">
<div v-for="item in achievements" :key="item.title" class="achievement-row">
<h4>{{ item.title }}</h4>
<p>{{ item.desc }}</p>
<span class="achv-icon"><CvIcon name="verified" /></span>
<div>
<h4>{{ item.title }}</h4>
<p>{{ item.desc }}</p>
</div>
</div>
</section>
@@ -354,8 +358,13 @@ const additionalExperiences: AdditionalExperience[] = [
<span>{{ lang.name }}</span>
<span class="tag">{{ lang.level }}</span>
</p>
<div class="meter meter-lg">
<div class="meter-fill" :style="{ width: (lang.rating / LANGUAGE_MAX) * 100 + '%' }"></div>
<div class="tally tally-lg">
<span
v-for="n in LANGUAGE_MAX"
:key="n"
class="tally-tick"
:class="{ filled: n <= lang.rating }"
></span>
</div>
</div>
</section>
@@ -365,8 +374,13 @@ const additionalExperiences: AdditionalExperience[] = [
<div class="skill-grid">
<div v-for="skill in skills" :key="skill.name" class="skill-row">
<span class="skill-name">{{ skill.name }}</span>
<div class="meter">
<div class="meter-fill" :style="{ width: (skill.rating / SKILL_MAX) * 100 + '%' }"></div>
<div class="tally">
<span
v-for="n in SKILL_MAX"
:key="n"
class="tally-tick"
:class="{ filled: n <= skill.rating }"
></span>
</div>
</div>
</div>
@@ -409,16 +423,18 @@ const additionalExperiences: AdditionalExperience[] = [
.profile-top {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.25rem;
gap: 1.1rem;
margin-bottom: 1rem;
}
.profile-top h1 {
font-size: 1.4rem;
margin-bottom: 0.4rem;
margin-bottom: 0.45rem;
letter-spacing: 0.01em;
}
.avatar {
display: block;
width: 76px;
height: 76px;
border-radius: 50%;
@@ -428,18 +444,48 @@ const additionalExperiences: AdditionalExperience[] = [
flex-shrink: 0;
}
.role-badge {
display: inline-flex;
padding: 0.22rem 0.65rem;
border-radius: 3px;
background: var(--c-accent-bg);
color: var(--c-accent-strong);
font-size: 0.78rem;
font-weight: 500;
}
.ledger-rule {
height: 4px;
margin-bottom: 1.5rem;
border-top: 1.5px solid var(--c-border-hover);
border-bottom: 1px dashed var(--c-border);
}
.section-heading {
display: flex;
align-items: center;
gap: 0.45rem;
font-size: 1.1rem;
margin: 1.75rem 0 0.75rem;
gap: 0.55rem;
font-size: 1.08rem;
font-weight: 600;
margin: 2rem 0 0.85rem;
}
.section-heading::after {
content: '';
flex: 1;
height: 1px;
background: var(--c-border);
margin-left: 0.35rem;
}
.section-heading .cv-icon {
font-size: 0.85rem;
color: var(--c-accent-strong);
opacity: 0.85;
}
.card {
border-radius: 4px;
box-shadow: none;
}
.info-card {
@@ -451,6 +497,11 @@ const additionalExperiences: AdditionalExperience[] = [
margin-bottom: 0.75rem;
}
.contact-card {
border-color: var(--c-accent-soft);
box-shadow: 0 10px 28px rgba(61, 114, 180, 0.22);
}
.row {
display: flex;
justify-content: space-between;
@@ -465,6 +516,18 @@ const additionalExperiences: AdditionalExperience[] = [
margin-bottom: 0;
}
.info-card > .row {
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
border-bottom: 1px dashed var(--c-border);
}
.info-card > .row:last-child {
padding-bottom: 0;
margin-bottom: 0;
border-bottom: none;
}
.row strong {
color: var(--c-heading);
}
@@ -488,8 +551,30 @@ const additionalExperiences: AdditionalExperience[] = [
color: var(--c-text);
}
.achievement-row {
display: flex;
gap: 0.7rem;
align-items: flex-start;
}
.achievement-row + .achievement-row {
margin-top: 0.85rem;
padding-top: 0.85rem;
border-top: 1px dashed var(--c-border);
}
.achv-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.6rem;
height: 1.6rem;
flex-shrink: 0;
margin-top: 0.05rem;
border-radius: 50%;
background: var(--c-accent-bg);
color: var(--c-accent-strong);
font-size: 0.9rem;
}
.achievement-row h4 {
@@ -507,22 +592,24 @@ const additionalExperiences: AdditionalExperience[] = [
margin-top: 0.85rem;
}
/* rating meters */
.meter {
width: 100%;
height: 6px;
border-radius: 999px;
background: var(--c-bg-mute);
overflow: hidden;
/* rating tallies */
.tally {
display: flex;
gap: 3px;
}
.meter-fill {
height: 100%;
border-radius: 999px;
.tally-tick {
flex: 1;
height: 6px;
border-radius: 1px;
background: var(--c-bg-mute);
}
.tally-tick.filled {
background: var(--c-accent);
}
.meter-lg {
.tally-lg .tally-tick {
height: 7px;
}
@@ -536,6 +623,46 @@ const additionalExperiences: AdditionalExperience[] = [
margin: 0;
}
/* timeline (experience / education): a real chronological record, so the
rail + nodes carry meaning rather than decorate */
.timeline {
position: relative;
gap: 0.9rem;
padding-left: 1.1rem;
}
.timeline::before {
content: '';
position: absolute;
left: 3px;
top: 0.6rem;
bottom: 0.6rem;
width: 1px;
background: var(--c-border-hover);
}
.timeline > li {
position: relative;
}
.timeline > li::before {
content: '';
position: absolute;
left: -1.1rem;
top: 1.1rem;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--c-bg);
border: 1.5px solid var(--c-text-soft);
}
.timeline-current > li:first-child::before {
border-color: var(--c-accent-strong);
background: var(--c-accent-strong);
box-shadow: 0 0 0 3px var(--c-accent-bg);
}
.job-card {
padding: 0.9rem 1.1rem;
}
@@ -697,9 +824,4 @@ const additionalExperiences: AdditionalExperience[] = [
background-color: var(--c-bg-mute);
color: var(--c-text-soft);
}
.tag-accent {
background-color: var(--c-accent-bg);
color: var(--c-accent-strong);
}
</style>