5846c515f3
- 4 temas WordPress: persona-cv (activo), lumina, apex-cv, cvolio - Plugin pflores-cv-fields con CMB2 (100% gratuito) — admin editable - Generador PDF dinámico con Chromium headless (?cv_pdf=1) - Datos del CV de Patricia Flores cargados en CMB2 - URL dinámica con soporte proxy SSL (NPM/X-Forwarded-Proto) - WP Mail SMTP instalado (pendiente config SMTP) - Documentación completa: PROMPT_IA.md, DEPLOY_WEB.md, PRODUCCION.md, VERSIONADO.md
98 lines
3.7 KiB
JavaScript
98 lines
3.7 KiB
JavaScript
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
/* ── Header scroll ── */
|
|
const header = document.querySelector(".pn-header");
|
|
const scrollTop = document.querySelector(".pn-scroll-top");
|
|
window.addEventListener("scroll", () => {
|
|
const y = window.scrollY;
|
|
if (header) header.classList.toggle("is-scrolled", y > 60);
|
|
if (scrollTop) scrollTop.classList.toggle("is-visible", y > 400);
|
|
}, { passive: true });
|
|
|
|
/* ── Mobile nav ── */
|
|
const hamburger = document.querySelector(".pn-hamburger");
|
|
const nav = document.querySelector(".pn-nav");
|
|
if (hamburger && nav) {
|
|
hamburger.addEventListener("click", () => {
|
|
const open = nav.classList.toggle("is-open");
|
|
hamburger.setAttribute("aria-expanded", String(open));
|
|
const s = hamburger.querySelectorAll("span");
|
|
if (s.length === 3) {
|
|
s[0].style.transform = open ? "rotate(45deg) translate(5px, 5px)" : "";
|
|
s[1].style.opacity = open ? "0" : "";
|
|
s[2].style.transform = open ? "rotate(-45deg) translate(5px, -5px)" : "";
|
|
}
|
|
});
|
|
document.addEventListener("click", e => {
|
|
if (!nav.contains(e.target) && !hamburger.contains(e.target)) {
|
|
nav.classList.remove("is-open");
|
|
hamburger.setAttribute("aria-expanded", "false");
|
|
hamburger.querySelectorAll("span").forEach(s => { s.style.transform = ""; s.style.opacity = ""; });
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ── Nav active on scroll ── */
|
|
const navLinks = Array.from(document.querySelectorAll(".pn-nav__list a[href^='#']"));
|
|
const sections = Array.from(document.querySelectorAll("section[id]"));
|
|
if (navLinks.length && sections.length) {
|
|
const obs = new IntersectionObserver(entries => {
|
|
entries.forEach(e => {
|
|
if (e.isIntersecting) {
|
|
const id = "#" + e.target.id;
|
|
navLinks.forEach(l => l.classList.toggle("is-active", l.getAttribute("href") === id));
|
|
}
|
|
});
|
|
}, { rootMargin: "-20% 0px -70% 0px" });
|
|
sections.forEach(s => obs.observe(s));
|
|
}
|
|
navLinks.forEach(l => l.addEventListener("click", () => {
|
|
if (nav) nav.classList.remove("is-open");
|
|
}));
|
|
|
|
/* ── Animated counters ── */
|
|
const counters = Array.from(document.querySelectorAll(".pn-counter__val"));
|
|
const counterObs = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (!entry.isIntersecting) return;
|
|
counterObs.unobserve(entry.target);
|
|
const el = entry.target;
|
|
const end = parseInt(el.dataset.count, 10);
|
|
const duration = 1800;
|
|
const step = Math.ceil(end / (duration / 16));
|
|
let current = 0;
|
|
const tick = () => {
|
|
current = Math.min(current + step, end);
|
|
el.textContent = current.toLocaleString("es-EC");
|
|
if (current < end) requestAnimationFrame(tick);
|
|
};
|
|
requestAnimationFrame(tick);
|
|
});
|
|
}, { threshold: 0.3 });
|
|
counters.forEach(c => counterObs.observe(c));
|
|
|
|
/* ── Skill bars animation ── */
|
|
const skillBars = Array.from(document.querySelectorAll(".pn-skill__fill"));
|
|
const skillObs = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (!entry.isIntersecting) return;
|
|
skillObs.unobserve(entry.target);
|
|
const bar = entry.target;
|
|
const pct = bar.dataset.pct;
|
|
setTimeout(() => { bar.style.width = pct + "%"; }, 100);
|
|
});
|
|
}, { threshold: 0.2 });
|
|
skillBars.forEach(b => skillObs.observe(b));
|
|
|
|
/* ── Print ── */
|
|
document.querySelectorAll("[data-persona-print]").forEach(b => b.addEventListener("click", () => window.print()));
|
|
|
|
/* ── Scroll top ── */
|
|
if (scrollTop) {
|
|
scrollTop.addEventListener("click", e => {
|
|
e.preventDefault();
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
});
|
|
}
|
|
});
|