mirror of
https://github.com/Dvorinka/Portfolio.git
synced 2026-10-28 07:41:22 +00:00
Redesign to Vercel style, sync content with GitHub/LinkedIn
Monochrome Geist-based design replaces Material Design 3. Content updated to current roles (MyBox/ELEXIM DevOps, Výdoz, Vibly), corrected dates (P+P, ZEUS ended 2025), fixed GitHub/LinkedIn/X handles, new project screenshots. Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
734296f907
commit
8dd7d1f9ef
7 files changed
+679
-2257
No files matched your search
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 |
+410
-844
File diff suppressed because it is too large.
Load diff
@@ -1,226 +1,41 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const $HTML = document.documentElement;
|
// Theme: persisted in sessionStorage, dark default.
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.dataset.theme = sessionStorage.getItem("theme") || "dark";
|
||||||
|
|
||||||
if (sessionStorage.getItem("theme")) {
|
document.querySelector("[data-theme-btn]")?.addEventListener("click", () => {
|
||||||
$HTML.dataset.theme = sessionStorage.getItem("theme");
|
root.dataset.theme = root.dataset.theme === "dark" ? "light" : "dark";
|
||||||
} else {
|
sessionStorage.setItem("theme", root.dataset.theme);
|
||||||
$HTML.dataset.theme = "dark"; // Set the default theme to dark
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Resume timeline duration auto-update =====
|
|
||||||
function parseYearMonth(value) {
|
|
||||||
// value format: 'YYYY-MM' or 'present'
|
|
||||||
if (!value || value === 'present') return null;
|
|
||||||
const [y, m] = value.split('-').map(Number);
|
|
||||||
return { year: y, month: m };
|
|
||||||
}
|
|
||||||
|
|
||||||
function monthDiff(start, end) {
|
|
||||||
// start/end: {year, month}
|
|
||||||
return (end.year - start.year) * 12 + (end.month - start.month);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toYearMonth(date) {
|
|
||||||
return { year: date.getFullYear(), month: date.getMonth() + 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function humanizeMonthsEN(totalMonths) {
|
|
||||||
if (totalMonths < 0) return '';
|
|
||||||
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
|
|
||||||
function updateFooterCopyright() {
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
const footerText = document.querySelector('.footer p.body-medium');
|
|
||||||
if (footerText) {
|
|
||||||
footerText.innerHTML = `Copyright ${currentYear}. All rights reserved by <b><a href="https://tdvorak.dev">TDvorak.</a></b>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
updateDurations();
|
|
||||||
updateFooterCopyright();
|
|
||||||
// refresh once a day to keep counters accurate as months roll
|
|
||||||
setInterval(updateDurations, 24 * 60 * 60 * 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeTheme = () => {
|
// Footer year
|
||||||
$HTML.dataset.theme = sessionStorage.getItem("theme") === "dark";
|
document.getElementById("year").textContent = new Date().getFullYear();
|
||||||
sessionStorage.setItem("theme", $HTML.dataset.theme);
|
|
||||||
};
|
|
||||||
|
|
||||||
const $tabBtn = document.querySelectorAll("[data-tab-btn]");
|
// Contact form -> sendmail.tdvorak.dev
|
||||||
let [lastActiveTab] = document.querySelectorAll("[data-tab-content]");
|
const form = document.getElementById("contact-form");
|
||||||
let [lastActiveTabBtn] = $tabBtn;
|
const status = document.getElementById("form-status");
|
||||||
|
|
||||||
$tabBtn.forEach(item => {
|
form?.addEventListener("submit", async (e) => {
|
||||||
item.addEventListener("click", function () {
|
e.preventDefault();
|
||||||
lastActiveTab.classList.remove("active");
|
const btn = form.querySelector('button[type="submit"]');
|
||||||
lastActiveTabBtn.classList.remove("active");
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Sending…";
|
||||||
|
status.textContent = "";
|
||||||
|
status.className = "form-status mono";
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
document.createEvent('TouchEvent');
|
const params = new URLSearchParams(Object.fromEntries(new FormData(form)));
|
||||||
return true;
|
const res = await fetch(`https://sendmail.tdvorak.dev/send?${params}`, { method: "GET" });
|
||||||
} catch (e) {
|
if (!res.ok) throw new Error(await res.text() || "Send failed");
|
||||||
return false;
|
status.textContent = "Message sent. I'll get back to you soon.";
|
||||||
}
|
status.classList.add("ok");
|
||||||
};
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
const move = (e) => {
|
status.textContent = "Something went wrong — try again or email me directly.";
|
||||||
if (!isTouchDevice()) {
|
status.classList.add("err");
|
||||||
cursorX = e.clientX;
|
} finally {
|
||||||
cursorY = e.clientY;
|
btn.disabled = false;
|
||||||
cursor.style.left = `${cursorX}px`;
|
btn.textContent = "Send message";
|
||||||
cursor.style.top = `${cursorY}px`;
|
|
||||||
} else {
|
|
||||||
// Disable custom cursor on touch devices
|
|
||||||
cursor.style.display = "none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.style.zIndex = "9999";
|
|
||||||
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();
|
|
||||||
|
|
||||||
const submitButton = form.querySelector('button[type="submit"]');
|
|
||||||
const originalButtonText = submitButton.innerHTML;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (submitButton) {
|
|
||||||
submitButton.disabled = true;
|
|
||||||
submitButton.innerHTML = 'Odesílám...';
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
} else {
|
|
||||||
throw new Error(text || 'Něco se pokazilo. Zkuste to prosím znovu.');
|
|
||||||
}
|
|
||||||
} 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 {
|
|
||||||
if (submitButton) {
|
|
||||||
submitButton.disabled = false;
|
|
||||||
submitButton.innerHTML = originalButtonText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user