340 lines
12 KiB
PHP
340 lines
12 KiB
PHP
<?php
|
||
|
||
/**
|
||
* Content config:
|
||
*/
|
||
$cvData = json_decode(file_get_contents(__DIR__ . '/data.json'), true);
|
||
|
||
$isCvPath = strpos($_SERVER['REQUEST_URI'], '/cv') !== false;
|
||
|
||
$name = $cvData['name'] ?? '';
|
||
$headline = $cvData['headline'] ?? '';
|
||
$summary = $cvData['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'] ?? [];
|
||
|
||
/**
|
||
* Emoji icons for link pills
|
||
*/
|
||
$pill_icons = [
|
||
'Website' => '🌐',
|
||
'Email' => '✉️',
|
||
'Resume' => '📄',
|
||
'CV' => '📄',
|
||
'PDF' => '📑',
|
||
'Blog' => '✍️',
|
||
'Projects' => '📁',
|
||
'Code' => '💻',
|
||
'GitLab' => '🔶',
|
||
'GitHub' => '🐙',
|
||
];
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
/**
|
||
* Render a standardized pill link
|
||
*/
|
||
function render_pill(string $href, string $label, string $icon_emoji, string $class_suffix = ''): string {
|
||
$url_display = pillUrl($href);
|
||
$classes = $class_suffix ? "pill {$class_suffix}" : 'pill';
|
||
return sprintf(
|
||
'<a href="%s" target="_blank" class="%s">' .
|
||
'<span class="pill-icon">%s</span>' .
|
||
'<span class="pill-label">%s</span>' .
|
||
'<span class="pill-url">%s</span>' .
|
||
'</a>',
|
||
htmlspecialchars($href, ENT_QUOTES, 'UTF-8'),
|
||
$classes,
|
||
$icon_emoji,
|
||
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
|
||
htmlspecialchars($url_display, ENT_QUOTES, 'UTF-8')
|
||
);
|
||
}
|
||
|
||
|
||
/**
|
||
* CV Data
|
||
*/
|
||
// Map experience to expected structure
|
||
$cv_experience = array_map(function ($e) {
|
||
return [
|
||
'title' => $e['title'] ?? '',
|
||
'org' => $e['company'] ?? '',
|
||
'date' => $e['date'] ?? '',
|
||
'desc' => $e['description'] ?? '',
|
||
];
|
||
}, $cvData['cv']['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['cv']['education'] ?? []);
|
||
|
||
// Keep skills categories as provided
|
||
$cv_skills = $cvData['cv']['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="<?= $isCvPath ? 'cv-mode' : '' ?>">
|
||
|
||
<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 ($summary): ?>
|
||
<div class="hero-row hero-description">
|
||
<p><?= $summary ?></p>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="hero-row hero-contacts">
|
||
<?php foreach ($extra_contact_links as $label => $href): ?>
|
||
<?= render_pill($href, $label, $pill_icons[$label] ?? '🔗', 'contact-pill') ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="hero-row hero-nav">
|
||
<?php foreach ($nav_links as $label => $href): ?>
|
||
<?= render_pill(trim($href), $label, $pill_icons[$label] ?? '🔗', 'nav-pill') ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<?php if ($isCvPath && $highlights): ?>
|
||
<section class="card selected-work">
|
||
<h2>Selected Work</h2>
|
||
<?php foreach ($highlights as $h): ?>
|
||
<article class="highlight-item">
|
||
<h3><?= $h['title'] ?? '' ?><?php if ($h['date'] ?? ''): ?> <time class="highlight-date"><?= $h['date'] ?></time><?php endif; ?></h3>
|
||
<?php if ($h['description']): ?>
|
||
<?php if (is_array($h['description'])): ?>
|
||
<?php foreach ($h['description'] as $desc): ?>
|
||
<p class="highlight-description"><?= $desc ?></p>
|
||
<?php endforeach; ?>
|
||
<?php else: ?>
|
||
<p class="highlight-description"><?= $h['description'] ?></p>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</article>
|
||
<?php endforeach; ?>
|
||
<?php if ($cvData['cv']['philosophy'] ?? []): ?>
|
||
<p class="philosophy"><?= $cvData['cv']['philosophy'] ?></p>
|
||
<?php endif; ?>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($cvData['outside_work'] ?? []): ?>
|
||
<?php if ($isCvPath): ?>
|
||
<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 else: ?>
|
||
<section class="card personal">
|
||
<h2>Interests</h2>
|
||
<div class="personal-tags">
|
||
<?php foreach ($cvData['outside_work'] as $tag): ?>
|
||
<span class="personal-tag"><?= $tag ?></span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($isCvPath): ?>
|
||
<section class="card cv-container">
|
||
<div class="cv-content">
|
||
<?php
|
||
$firstJob = $cv_experience[0] ?? null;
|
||
if ($firstJob):
|
||
?>
|
||
<section class="cv-section first-experience">
|
||
<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">
|
||
<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">
|
||
<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">
|
||
<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>
|
||
<?php endif; ?>
|
||
</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');
|
||
</script>
|
||
<!-- Cloudflare Web Analytics -->
|
||
<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "90827cb740cd4b1db611be059f64daa9"}'></script><!-- End Cloudflare Web Analytics -->
|
||
<?php $file_mod_time = filemtime(__FILE__); ?>
|
||
<p class="page-footer">Modified: <?= date("Y-m-d", $file_mod_time) ?></p>
|
||
</body>
|
||
|
||
</html>
|