homepage/index.php
2026-05-21 15:11:07 +02:00

399 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Content config:
*/
$cvData = json_decode(file_get_contents(__DIR__ . '/cv.json'), true);
$name = $cvData['name'] ?? '';
$headline = $cvData['headline'] ?? '';
$role_summary = $cvData['role_summary'] ?? '';
$location = $cvData['location'] ?? '';
$photo_file = $cvData['photo'] ?? '';
$nav_links = [
'Blog' => 'https://wp.dejvino.com/',
'Projects' => 'https://projects.dejvino.com/',
'Code' => 'https://git.dejvino.cz/',
];
$highlights = $cvData['highlights'] ?? [];
$site_url = "https://www.dejvino.cz/";
$site_title = $name;
$site_desc = "$name - $headline";
$mailto_link = $cvData['contact_links']['Email'] ?? '';
if (preg_match('/mailto:([^@]+)/', $mailto_link, $m)) {
$email_user = $m[1];
} else {
$email_user = $cvData['email'] ?? '';
}
$extra_contact_links = $cvData['contact_links'] ?? [];
/**
* Icon SVGs for link pills
*/
$pill_icons = [
'Website' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
'Email' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>',
'GitLab' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>',
'GitHub' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0 1 12 6.836a9.59 9.59 0 0 1 2.504.337c1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>',
'CV' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
'PDF' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M9 15v-2h2a1.5 1.5 0 0 1 0 3H9v-1c0-.55.45-1 1-1h1"/><line x1="9" y1="13" x2="11" y2="13"/></svg>',
'Blog' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
'Projects' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
'Code' => '<svg class="pill-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
];
/**
* Extract a readable URL from a href (strip protocol, trailing slash)
*/
function pillUrl(string $href): string {
$url = $href;
if (preg_match('/^[a-z]+:\/\//i', $url)) {
$url = preg_replace('/^[a-z]+:\/\//i', '', $url);
}
$url = rtrim($url, '/');
return strlen($url) > 30 ? mb_substr($url, 0, 27) . '…' : $url;
}
/**
* CV Data
*/
// Map experience to expected structure
$cv_experience = array_map(function ($e) {
return [
'title' => $e['title'] ?? '',
'org' => $e['organization'] ?? '',
'date' => $e['date'] ?? '',
'desc' => $e['description'] ?? '',
];
}, $cvData['experience'] ?? []);
// Helper to calculate years between start and end dates
function calculateYears($dateStr) {
// Extract start and end years, support 'present' or 'current'
$dateStr = str_replace(['', '—', '', '-'], '-', $dateStr);
$parts = array_map('trim', explode('-', $dateStr, 2));
if (count($parts) < 2) {
return 0;
}
$start = (int)$parts[0];
$endRaw = strtolower($parts[1]);
$end = ($endRaw === 'present' || $endRaw === 'current') ? (int)date('Y') : (int)$endRaw;
$years = $end - $start;
if ($years <= 1) {
return "1 year";
}
return "$years years";
}
// Map education to expected structure
$cv_education = array_map(function ($e) {
return [
'title' => $e['degree'] ?? '',
'org' => $e['institution'] ?? '',
'date' => $e['date'] ?? '',
'desc' => $e['thesis'] ?? '',
];
}, $cvData['education'] ?? []);
// Keep skills categories as provided
$cv_skills = $cvData['skills'] ?? [];
/**
* Page design:
* - clean and simple, human-maintainable
* - no dependencies
* - single-file website
*/
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= $site_title ?></title>
<link rel="canonical" href="<?= $site_url ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="<?= $site_desc ?>">
<meta name="author" content="<?= $name ?>">
<!-- Open Graph / Social Media -->
<meta property="og:type" content="website">
<meta property="og:url" content="<?= $site_url ?>">
<meta property="og:title" content="<?= $name ?>">
<meta property="og:description" content="<?= $headline ?>">
<style>
<?php
include 'style.css';
?>
</style>
<link rel="shortcut icon" href="favicon.ico">
</head>
<body class="preload">
<main class="bento-grid">
<section class="card hero">
<div class="hero-row hero-identity">
<?php
if (file_exists($photo_file)) {
$avatarBase64 = base64_encode(file_get_contents($photo_file));
echo '<img src="data:image/png;base64,' . $avatarBase64 . '" alt="' . $name . '" class="avatar" width="80" height="106" />';
} else {
echo '<div class="avatar-placeholder"></div>';
}
?>
<div class="hero-name">
<h1><?= $name ?></h1>
<?php if ($headline): ?>
<p class="headline"><?= $headline ?></p>
<?php endif; ?>
<?php if ($location): ?>
<p class="location"><?= $location ?></p>
<?php endif; ?>
</div>
</div>
<?php if ($role_summary): ?>
<div class="hero-row hero-description">
<p><?= $role_summary ?></p>
</div>
<?php endif; ?>
<div class="hero-row hero-contacts">
<?php foreach ($extra_contact_links as $label => $href): ?>
<a href="<?= $href ?>" target="_blank" class="pill contact-pill">
<?= $pill_icons[$label] ?? '' ?>
<span class="pill-label"><?= $label ?></span>
<span class="pill-url"><?= pillUrl($href) ?></span>
</a>
<?php endforeach; ?>
</div>
<div class="hero-row hero-nav">
<?php foreach ($nav_links as $label => $href): ?>
<a href="<?= trim($href) ?>" target="_blank" class="pill nav-pill">
<?= $pill_icons[$label] ?? '' ?>
<span class="pill-label"><?= $label ?></span>
<span class="pill-url"><?= pillUrl($href) ?></span>
</a>
<?php endforeach; ?>
</div>
</section>
<?php if ($highlights): ?>
<section class="card selected-work">
<h2>Selected Work</h2>
<?php foreach ($highlights as $h): ?>
<article class="highlight-item">
<h3><?= $h['title'] ?? '' ?></h3>
<?php if ($h['subtitle'] ?? ''): ?>
<p class="highlight-subtitle"><?= $h['subtitle'] ?></p>
<?php endif; ?>
<?php if ($h['description'] ?? ''): ?>
<p class="highlight-description"><?= $h['description'] ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
</section>
<?php endif; ?>
<?php if ($cvData['outside_work'] ?? []): ?>
<section class="card personal">
<h2>Outside Work</h2>
<div class="personal-tags">
<?php foreach ($cvData['outside_work'] as $tag): ?>
<span class="personal-tag"><?= $tag ?></span>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<section class="card cv-container">
<label class="cv-header" for="cv-toggle">
<div class="cv-toggle-container">
<span id="toggle-label-text">Looking to hire?</span>
<span class="switch">
<input type="checkbox" id="cv-toggle">
<span class="slider"></span>
</span>
</div>
</label>
<div class="cv-content">
<?php $file_mod_time = filemtime(__FILE__); ?>
<p class="cv-modified hidden hidden-toggle">Modified: <?= date("Y-m-d", $file_mod_time) ?></p>
<?php
$firstJob = $cv_experience[0] ?? null;
if ($firstJob):
?>
<section class="cv-section first-experience hidden hidden-toggle">
<h2>Latest Experience</h2>
<article class="cv-item">
<header class="cv-item-header">
<h3 class="cv-title"><?= $firstJob['title'] ?></h3>
<span class="cv-date"><?= $firstJob['date'] ?> (<?= calculateYears($firstJob['date']) ?>)</span>
</header>
<p class="cv-org"><?= $firstJob['org'] ?></p>
<p class="cv-desc"><?= $firstJob['desc'] ?></p>
</article>
</section>
<?php endif; ?>
<section class="cv-section skills hidden hidden-toggle">
<h2>Skills</h2>
<div class="skill-categories">
<?php
$global_i = 0;
foreach ($cv_skills as $category => $skills): ?>
<div class="skill-group">
<h3><?= $category ?></h3>
<div class="skill-tags">
<?php foreach ($skills as $skill):
$fontSize = 0.85 + (($global_i * 3) % 5) * 0.2;
$opacity = 0.6 + (($global_i * 2) % 4) * 0.1;
?>
<span class="tag" style="transition-delay: <?= 0.6 + ($global_i * 0.05) ?>s; font-size: <?= $fontSize ?>rem; opacity: <?= $opacity ?>;"><?= $skill ?></span>
<?php $global_i++;
endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="cv-section experience hidden hidden-toggle">
<h2>Past Experience</h2>
<?php foreach (array_slice($cv_experience, 1) as $job): ?>
<article class="cv-item">
<header class="cv-item-header">
<h3 class="cv-title"><?= $job['title'] ?></h3>
<span class="cv-date"><?= $job['date'] ?> (<?= calculateYears($job['date']) ?>)</span>
</header>
<p class="cv-org"><?= $job['org'] ?></p>
<p class="cv-desc"><?= $job['desc'] ?></p>
</article>
<?php endforeach; ?>
</section>
<section class="cv-section education hidden hidden-toggle">
<h2>Education</h2>
<?php foreach ($cv_education as $edu): ?>
<article class="cv-item">
<header class="cv-item-header">
<h3 class="cv-title"><?= $edu['title'] ?></h3>
<time class="cv-date"><?= $edu['date'] ?></time>
</header>
<p class="cv-org"><?= $edu['org'] ?></p>
<?php if ($edu['desc']): ?>
<p class="cv-desc"><?= $edu['desc'] ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
</section>
</div>
</section>
</main>
<script>
((u, d, t) => {
const addr = (atob(u) + '@' + d + '.' + t).trim();
const el = document.getElementById("contact-mail");
if (el) el.innerHTML = `<a href="mailto:${addr}">${addr}</a>`;
})('<?= base64_encode($email_user) ?>', 'dejvino', 'cz');
const animationDuration = 500; // Matches CSS animation duration (0.5s)
const staggerDelay = 200; // Delay between each panel unrolling (ms)
const toggleCV = (show, updateHash = true, animate = true) => {
const grid = document.querySelector('.bento-grid');
const labelText = document.getElementById('toggle-label-text');
if (labelText) labelText.innerText = show ? 'Here is my Resume' : 'Looking to hire?';
const cvPanels = Array.from(document.querySelectorAll('.hidden-toggle')).filter(el => el);
if (show) {
grid.classList.add('cv-mode');
cvPanels.forEach((el, index) => {
const reveal = () => {
el.classList.remove('hidden', 'fade-out', 'reveal');
void el.offsetWidth; // Force reflow
el.classList.add('reveal');
};
if (animate) setTimeout(reveal, index * staggerDelay);
else reveal();
});
} else {
const panels = [...cvPanels].reverse();
panels.forEach((el, index) => {
const hide = () => {
el.classList.remove('reveal');
if (animate) {
el.classList.add('fade-out');
setTimeout(() => {
el.classList.add('hidden');
el.classList.remove('fade-out');
}, animationDuration);
} else {
el.classList.add('hidden');
}
};
if (animate) setTimeout(hide, index * staggerDelay);
else hide();
});
if (animate) {
// Remove cv-mode only after the longest-running shrink animation finishes
const totalRollupTime = (cvPanels.length * staggerDelay) + animationDuration;
setTimeout(() => grid.classList.remove('cv-mode'), totalRollupTime);
} else {
grid.classList.remove('cv-mode');
}
}
if (updateHash) {
window.location.hash = show ? 'CV' : '';
}
// Keep toggle in sync
const toggle = document.getElementById('cv-toggle');
if (toggle) toggle.checked = show;
};
// Initialize state from URL
const checkHash = (animate = false) => toggleCV(window.location.hash === '#CV', false, animate);
window.addEventListener('load', () => {
checkHash(false);
// Small delay to ensure initial styles are applied before enabling transitions
setTimeout(() => document.body.classList.remove('preload'), 20);
});
window.addEventListener('hashchange', () => checkHash(true));
const cvToggleInput = document.getElementById('cv-toggle');
cvToggleInput?.addEventListener('change', (e) => {
toggleCV(e.target.checked);
// Only scroll to top if we are collapsing the CV and the user is scrolled down
if (!e.target.checked && window.scrollY > 200) {
window.scrollTo({
top: document.querySelector('.cv-container').offsetTop - 20,
behavior: 'smooth'
});
}
});
</script>
<!-- Cloudflare Web Analytics -->
<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "90827cb740cd4b1db611be059f64daa9"}'></script><!-- End Cloudflare Web Analytics -->
</body>
</html>