feat(all): 1-st site version
This commit is contained in:
commit
4ee119a960
23 files changed
+1797
No files matched your search
@@ -0,0 +1,153 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
/* ---------- мобильное меню ---------- */
|
||||
const toggle = document.getElementById("navToggle");
|
||||
const links = document.getElementById("navLinks");
|
||||
|
||||
if (toggle && links) {
|
||||
toggle.addEventListener("click", () => {
|
||||
const open = links.classList.toggle("open");
|
||||
toggle.classList.toggle("open", open);
|
||||
toggle.setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
|
||||
links.querySelectorAll("a").forEach((a) =>
|
||||
a.addEventListener("click", () => {
|
||||
links.classList.remove("open");
|
||||
toggle.classList.remove("open");
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- тень хедера при скролле ---------- */
|
||||
const header = document.querySelector(".site-header");
|
||||
const onScroll = () => header && header.classList.toggle("scrolled", window.scrollY > 8);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
/* ---------- появление блоков ---------- */
|
||||
const reveal = document.querySelectorAll("[data-reveal]");
|
||||
if ("IntersectionObserver" in window) {
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry, i) => {
|
||||
if (entry.isIntersecting) {
|
||||
const el = entry.target;
|
||||
setTimeout(() => el.classList.add("visible"), i * 70);
|
||||
io.unobserve(el);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.12, rootMargin: "0px 0px -40px 0px" }
|
||||
);
|
||||
reveal.forEach((el) => io.observe(el));
|
||||
} else {
|
||||
reveal.forEach((el) => el.classList.add("visible"));
|
||||
}
|
||||
|
||||
/* ---------- счётчики ---------- */
|
||||
const counters = document.querySelectorAll("[data-count]");
|
||||
const animateCount = (el) => {
|
||||
const target = parseInt(el.dataset.count, 10);
|
||||
const duration = 1400;
|
||||
const start = performance.now();
|
||||
const step = (now) => {
|
||||
const p = Math.min((now - start) / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - p, 3);
|
||||
el.textContent = Math.round(target * eased).toString();
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
const co = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCount(entry.target);
|
||||
co.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
counters.forEach((el) => co.observe(el));
|
||||
} else {
|
||||
counters.forEach((el) => (el.textContent = el.dataset.count));
|
||||
}
|
||||
|
||||
/* ---------- печатающийся терминал ---------- */
|
||||
const typed = document.getElementById("typed");
|
||||
if (typed) {
|
||||
const lines = [
|
||||
{ t: '<span class="prompt">$</span> docker ps --format "table {{.Names}}\\t{{.Status}}"', d: 22 },
|
||||
{ t: "\n", d: 0 },
|
||||
{ t: '<span class="dim">NAMES STATUS</span>\n', d: 14 },
|
||||
{ t: "gitea <span class=\"ok\">Up 14 months</span>\n", d: 14 },
|
||||
{ t: "matrix <span class=\"ok\">Up 14 months</span>\n", d: 14 },
|
||||
{ t: "sharkey <span class=\"ok\">Up 14 months</span>\n", d: 14 },
|
||||
{ t: "copyparty <span class=\"ok\">Up 14 months</span>\n", d: 14 },
|
||||
{ t: "grafana <span class=\"ok\">Up 14 months</span>\n", d: 14 },
|
||||
{ t: "4get <span class=\"ok\">Up 14 months</span>\n", d: 14 },
|
||||
{ t: '<span class="prompt">$</span> uptime', d: 22 },
|
||||
{ t: '\n <span class="ok">all systems nominal</span>', d: 14 },
|
||||
{ t: '<span class="prompt">$</span> ', d: 22 },
|
||||
];
|
||||
|
||||
// токенизируем: HTML-теги — атомарные, текст — посимвольно
|
||||
const tokenize = (str) => str.match(/<[^>]+>|./gs) || [];
|
||||
|
||||
let lineIndex = 0;
|
||||
let tokens = [];
|
||||
let pos = 0;
|
||||
let html = "";
|
||||
|
||||
const render = () => {
|
||||
typed.innerHTML = html + '<span class="cursor"></span>';
|
||||
};
|
||||
|
||||
const typeNext = () => {
|
||||
if (lineIndex >= lines.length) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pos === 0) tokens = tokenize(lines[lineIndex].t);
|
||||
|
||||
if (pos < tokens.length) {
|
||||
html += tokens[pos++];
|
||||
render();
|
||||
const step = tokens[pos - 1].startsWith("<")
|
||||
? 0
|
||||
: lines[lineIndex].d + Math.random() * 35;
|
||||
setTimeout(typeNext, step);
|
||||
} else {
|
||||
lineIndex++;
|
||||
pos = 0;
|
||||
setTimeout(typeNext, 190);
|
||||
}
|
||||
};
|
||||
|
||||
const startTyping = () => setTimeout(typeNext, 500);
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
const to = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
startTyping();
|
||||
to.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
to.observe(typed);
|
||||
} else {
|
||||
startTyping();
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user