405 lines
14 KiB
PHP
405 lines
14 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'] ?? [];
|
||
|
||
/**
|
||
* Sourced Interests Showcase (from data.json)
|
||
*/
|
||
$interests_showcase = [];
|
||
if (!empty($cvData['interests'])) {
|
||
foreach ($cvData['interests'] as $item) {
|
||
$img = $item['image'] ?? null;
|
||
$has_image = $img && file_exists(__DIR__ . '/' . $img);
|
||
$interests_showcase[] = [
|
||
'name' => $item['name'] ?? '',
|
||
'image' => $has_image ? $img : null,
|
||
'caption' => $item['caption'] ?? ''
|
||
];
|
||
}
|
||
}
|
||
|
||
$site_url = "https://www.dejvino.cz/";
|
||
$site_title = $name;
|
||
$site_desc = "$name - $headline";
|
||
|
||
$extra_contact_links = $cvData['contact_links'] ?? [];
|
||
|
||
/**
|
||
* Emoji icons for link pills
|
||
*/
|
||
$pill_icons = [
|
||
'Website' => '🌐',
|
||
'Email' => '✉️',
|
||
'Resume' => '📄',
|
||
'CV' => '📄',
|
||
'PDF' => '📑',
|
||
'Blog' => '✍️',
|
||
'Projects' => '📁',
|
||
'Code' => '💻',
|
||
'GitLab' => '🔶',
|
||
'GitHub' => '🐙',
|
||
];
|
||
|
||
/** MACHINE READABLE ENDPOINTS */
|
||
$parsedUrl = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||
if (in_array($parsedUrl, ['/api/data.json', '/api/data.json/'], true)) {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Access-Control-Allow-Origin: *');
|
||
readfile(__DIR__ . '/data.json');
|
||
exit;
|
||
}
|
||
if (in_array($parsedUrl, ['/llms.txt', '/llms.txt/'], true)) {
|
||
header('Content-Type: text/plain; charset=utf-8');
|
||
echo '# ' . $site_url . PHP_EOL;
|
||
echo '> ' . $name . ' — ' . $headline . PHP_EOL;
|
||
echo PHP_EOL;
|
||
echo $summary . PHP_EOL;
|
||
echo PHP_EOL;
|
||
echo '## Machine-readable data' . PHP_EOL;
|
||
echo '- JSON: ' . $site_url . 'api/data.json' . PHP_EOL;
|
||
echo PHP_EOL;
|
||
echo '## Links' . PHP_EOL;
|
||
foreach ($nav_links as $label => $href) {
|
||
echo '- ' . $label . ': ' . $href . PHP_EOL;
|
||
}
|
||
if (!empty($cvData['contact_links'])) {
|
||
echo PHP_EOL;
|
||
echo '## Contact' . PHP_EOL;
|
||
foreach ($cvData['contact_links'] as $label => $href) {
|
||
echo '- ' . $label . ': ' . $href . PHP_EOL;
|
||
}
|
||
}
|
||
exit;
|
||
}
|
||
/** END OF MACHINE READABLE ENDPOINTS */
|
||
|
||
/**
|
||
* 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'] ?? [];
|
||
|
||
// JSON-LD structured data for machine readability (AI crawlers, schema.org)
|
||
$jsonLd = [
|
||
'@context' => 'https://schema.org',
|
||
'@type' => 'Person',
|
||
'name' => $name,
|
||
'jobTitle' => $headline,
|
||
'description' => $summary,
|
||
'url' => $site_url,
|
||
'email' => $cvData['email'] ?? '',
|
||
'telephone' => $cvData['phone'] ?? '',
|
||
'image' => $site_url . $photo_file,
|
||
'address' => [
|
||
'@type' => 'PostalAddress',
|
||
'addressLocality' => $location,
|
||
],
|
||
'sameAs' => array_values($nav_links),
|
||
'knowsAbout' => [],
|
||
'alumniOf' => [],
|
||
'worksFor' => [],
|
||
];
|
||
foreach ($cvData['cv']['skills'] ?? [] as $category => $skills) {
|
||
$jsonLd['knowsAbout'] = array_merge($jsonLd['knowsAbout'], $skills);
|
||
}
|
||
foreach ($cvData['interests'] ?? [] as $interest) {
|
||
$jsonLd['knowsAbout'][] = $interest['name'] ?? '';
|
||
}
|
||
$firstExp = $cvData['cv']['experience'][0] ?? null;
|
||
if ($firstExp) {
|
||
$jsonLd['worksFor'] = [
|
||
'@type' => 'Organization',
|
||
'name' => str_replace('|', ' / ', $firstExp['company'] ?? ''),
|
||
'description' => $firstExp['title'] ?? '',
|
||
];
|
||
} else {
|
||
unset($jsonLd['worksFor']);
|
||
}
|
||
foreach ($cvData['cv']['education'] ?? [] as $edu) {
|
||
$jsonLd['alumniOf'][] = [
|
||
'@type' => 'CollegeOrUniversity',
|
||
'name' => $edu['institution'] ?? '',
|
||
'description' => ($edu['degree'] ?? '') . ($edu['field'] ? ' in ' . $edu['field'] : ''),
|
||
];
|
||
}
|
||
$jsonLdEncoded = json_encode($jsonLd, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
|
||
/**
|
||
* 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 ?>">
|
||
|
||
<script type="application/ld+json"><?= $jsonLdEncoded ?></script>
|
||
|
||
<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>
|
||
|
||
<button class="print-button" onclick="window.print()">
|
||
🖨️ Print CV
|
||
</button>
|
||
|
||
<?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' . ($label === 'CV' || $label === 'Resume' ? ' pill-cv' : '')) ?>
|
||
<?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): ?>
|
||
<?php if ($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']): ?>
|
||
<div class="highlight-descriptions">
|
||
<?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; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</article>
|
||
<?php endforeach; ?>
|
||
<?php if ($cvData['cv']['philosophy'] ?? ''): ?>
|
||
<blockquote class="philosophy"><?= $cvData['cv']['philosophy'] ?></blockquote>
|
||
<?php endif; ?>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<section class="card skills">
|
||
<h2>Skills</h2>
|
||
<div class="skill-categories">
|
||
<?php foreach ($cv_skills as $category => $skills): ?>
|
||
<div class="skill-group">
|
||
<h3><?= $category ?></h3>
|
||
<div class="skill-tags">
|
||
<?php foreach ($skills as $skill): ?>
|
||
<span class="tag"><?= $skill ?></span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
|
||
<?php
|
||
$firstJob = $cv_experience[0] ?? null;
|
||
if ($firstJob):
|
||
?>
|
||
<section class="card 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="card 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="card 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>
|
||
|
||
<?php if ($interests_showcase): ?>
|
||
<section class="card personal">
|
||
<h2>Interests</h2>
|
||
<div class="personal-tags">
|
||
<?php foreach ($interests_showcase as $item): ?>
|
||
<span class="personal-tag"><?= htmlspecialchars($item['name']) ?></span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</main>
|
||
|
||
<!-- 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>
|