Merge pull request #1 from Dvorinka/i-have-really-outdated-portfol

Vercel-style redesign + content sync with GitHub/LinkedIn
This commit is contained in:
Tomáš Dvořák authored and GitHub committed 2026-09-20 12:04:22 +02:00
commit 16f9c26a00
12 files changed
+718 -2204

No files matched your search

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+383 -805
View File
File diff suppressed because it is too large. Load diff
+45 -206
View File
@@ -1,226 +1,65 @@
"use strict"; "use strict";
const $HTML = document.documentElement; document.documentElement.classList.add("js");
if (sessionStorage.getItem("theme")) { // Theme: persisted in sessionStorage, dark default.
$HTML.dataset.theme = sessionStorage.getItem("theme"); const root = document.documentElement;
} else { root.dataset.theme = sessionStorage.getItem("theme") || "dark";
$HTML.dataset.theme = "dark"; // Set the default theme to dark
}
// ===== Resume timeline duration auto-update ===== document.querySelector("[data-theme-btn]")?.addEventListener("click", () => {
function parseYearMonth(value) { root.dataset.theme = root.dataset.theme === "dark" ? "light" : "dark";
// value format: 'YYYY-MM' or 'present' sessionStorage.setItem("theme", root.dataset.theme);
if (!value || value === 'present') return null; });
const [y, m] = value.split('-').map(Number);
return { year: y, month: m };
}
function monthDiff(start, end) { // Footer year
// start/end: {year, month} document.getElementById("year").textContent = new Date().getFullYear();
return (end.year - start.year) * 12 + (end.month - start.month);
}
function toYearMonth(date) { // Card spotlight: track cursor into --mx/--my
return { year: date.getFullYear(), month: date.getMonth() + 1 }; for (const card of document.querySelectorAll(".card")) {
} card.addEventListener("mousemove", (e) => {
const r = card.getBoundingClientRect();
function humanizeMonthsEN(totalMonths) { card.style.setProperty("--mx", `${e.clientX - r.left}px`);
if (totalMonths < 0) return ''; card.style.setProperty("--my", `${e.clientY - r.top}px`);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
const yearStr = years === 0 ? '' : years === 1 ? '1 year' : `${years} years`;
const monthStr = months === 0 ? '' : months === 1 ? '1 month' : `${months} months`;
if (yearStr && monthStr) return `${yearStr} ${monthStr}`;
return yearStr || monthStr || '0 months';
}
function updateDurations() {
const nowYM = toYearMonth(new Date());
document.querySelectorAll('.duration').forEach(el => {
const startAttr = el.getAttribute('data-start');
const endAttr = el.getAttribute('data-end');
const startYM = parseYearMonth(startAttr);
const endYM = endAttr === 'present' ? nowYM : parseYearMonth(endAttr);
if (!startYM || !endYM) return;
const diff = monthDiff(startYM, endYM);
el.textContent = humanizeMonthsEN(diff);
}); });
} }
// Update footer copyright with current year // Staggered reveal on scroll
function updateFooterCopyright() { const reveal = new IntersectionObserver((entries) => {
const currentYear = new Date().getFullYear(); for (const en of entries) {
const footerText = document.querySelector('.footer p.body-medium'); if (en.isIntersecting) {
if (footerText) { const cards = [...en.target.parentElement.children];
footerText.innerHTML = `Copyright ${currentYear}. All rights reserved by <b><a href="https://tdvorak.dev">TDvorak.</a></b>`; en.target.style.transitionDelay = `${cards.indexOf(en.target) * 60}ms`;
en.target.classList.add("in");
reveal.unobserve(en.target);
} }
}
document.addEventListener('DOMContentLoaded', () => {
updateDurations();
updateFooterCopyright();
// refresh once a day to keep counters accurate as months roll
setInterval(updateDurations, 24 * 60 * 60 * 1000);
});
const changeTheme = () => {
$HTML.dataset.theme = sessionStorage.getItem("theme") === "dark";
sessionStorage.setItem("theme", $HTML.dataset.theme);
};
const $tabBtn = document.querySelectorAll("[data-tab-btn]");
let [lastActiveTab] = document.querySelectorAll("[data-tab-content]");
let [lastActiveTabBtn] = $tabBtn;
$tabBtn.forEach(item => {
item.addEventListener("click", function () {
lastActiveTab.classList.remove("active");
lastActiveTabBtn.classList.remove("active");
const $tabContent = document.querySelector(`[data-tab-content="${item.dataset.tabBtn}"]`);
$tabContent.classList.add("active");
this.classList.add("active");
lastActiveTab = $tabContent;
lastActiveTabBtn = this;
});
});
const cursor = document.getElementById('cursor');
cursor.style.position = 'fixed'; // Add 'position: fixed' to ensure absolute positioning
let cursorX = 0,
cursorY = 0;
const isTouchDevice = () => {
try {
document.createEvent('TouchEvent');
return true;
} catch (e) {
return false;
} }
}; }, { threshold: 0.15 });
document.querySelectorAll(".card").forEach((c) => reveal.observe(c));
const move = (e) => { // Contact form -> sendmail.tdvorak.dev
if (!isTouchDevice()) { const form = document.getElementById("contact-form");
cursorX = e.clientX; const status = document.getElementById("form-status");
cursorY = e.clientY;
cursor.style.left = `${cursorX}px`;
cursor.style.top = `${cursorY}px`;
} else {
// Disable custom cursor on touch devices
cursor.style.display = "none";
return;
}
cursor.style.zIndex = "9999"; form?.addEventListener("submit", async (e) => {
cursor.style.display = "block";
};
document.addEventListener('mouseenter', (e) => {
cursor.style.zIndex = "9999";
move(e);
});
document.addEventListener('mouseleave', () => {
if (isTouchDevice()) {
cursor.style.display = "none";
}
});
document.addEventListener('mousemove', (e) => {
move(e);
});
document.addEventListener('touchmove', (e) => {
move(e);
cursor.style.display = "none";
});
document.addEventListener('touchend', () => {
cursor.style.display = "none";
});
document.addEventListener('touchstart', (e) => {
cursor.style.display = "none"; // Hide the cursor on touchstart
const scrollX = e.touches[0].pageX;
const scrollY = e.touches[0].pageY;
move({ clientX: scrollX, clientY: scrollY });
});
// animate border
const borderAnimation = () => {
requestAnimationFrame(borderAnimation);
};
requestAnimationFrame(borderAnimation);
// handle scroll event
document.addEventListener('scroll', (e) => {
const scrollX = e.pageX;
const scrollY = e.pageY;
move({ clientX: scrollX, clientY: scrollY });
});
function sendEmail() {
emailjs.send('service_j9prncb', 'template_drg2vil', {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
subject: document.getElementById('subject').value,
message: document.getElementById('message').value
})
.then(function(response) {
console.log('SUCCESS!', response.status, response.text);
}, function(error) {
console.log('FAILED...', error);
});
}
// ===== Contact form submission =====
document.addEventListener('DOMContentLoaded', () => {
// Contact form submission
const form = document.getElementById('contact-form');
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const btn = form.querySelector('button[type="submit"]');
const submitButton = form.querySelector('button[type="submit"]'); btn.disabled = true;
const originalButtonText = submitButton.innerHTML; btn.textContent = "Sending…";
status.textContent = "";
status.className = "form-status mono";
try { try {
if (submitButton) { const params = new URLSearchParams(Object.fromEntries(new FormData(form)));
submitButton.disabled = true; const res = await fetch(`https://sendmail.tdvorak.dev/send?${params}`, { method: "GET" });
submitButton.innerHTML = 'Odesílám...'; if (!res.ok) throw new Error(await res.text() || "Send failed");
} status.textContent = "Message sent. I'll get back to you soon.";
status.classList.add("ok");
const data = Object.fromEntries(new FormData(form));
const params = new URLSearchParams(data);
// Preserve existing query string too
const existing = window.location.search.replace(/^\?/, '');
const url = 'https://sendmail.tdvorak.dev/send' + (existing ? ('?' + existing + '&' + params.toString()) : ('?' + params.toString()));
const res = await fetch(url, { method: 'GET' });
const text = await res.text();
if (res.ok) {
alert('Děkujeme za vaši zprávu! Brzy se vám ozveme zpět.');
form.reset(); form.reset();
} else { } catch (err) {
throw new Error(text || 'Něco se pokazilo. Zkuste to prosím znovu.'); status.textContent = "Something went wrong — try again or email me directly.";
} status.classList.add("err");
} catch (error) {
console.error('Error:', error);
alert(error.message || 'Při odesílání zprávy došlo k chybě. Zkuste to prosím znovu později.');
} finally { } finally {
if (submitButton) { btn.disabled = false;
submitButton.disabled = false; btn.textContent = "Send message";
submitButton.innerHTML = originalButtonText;
}
}
});
} }
}); });
+290 -1193
View File
File diff suppressed because it is too large. Load diff