// === App root + global effects (ink cursor, easter egg) ===
const InkCursor = () => {
React.useEffect(() => {
let last = 0;
const onMove = (e) => {
const t = performance.now();
if (t - last < 36) return;
last = t;
const d = document.createElement("div");
d.className = "ink-drop";
const sz = 6 + Math.random() * 8;
d.style.width = sz + "px";
d.style.height = sz + "px";
d.style.left = e.clientX + "px";
d.style.top = e.clientY + "px";
document.body.appendChild(d);
setTimeout(() => d.remove(), 850);
};
const onClick = (e) => {
const s = document.createElement("div");
s.className = "ink-splatter";
s.style.left = e.clientX + "px";
s.style.top = e.clientY + "px";
s.innerHTML = ``;
document.body.appendChild(s);
setTimeout(() => s.remove(), 750);
};
window.addEventListener("mousemove", onMove);
window.addEventListener("click", onClick);
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("click", onClick);
};
}, []);
return null;
};
const EasterEgg = () => {
React.useEffect(() => {
let buf = "";
let timer = null;
const triggers = ["nihao", "你好"];
const fire = () => {
const chars = ["印", "好", "赞", "推", "学", "卷", "躺", "搭", "六", "包"];
for (let i = 0; i < 28; i++) {
const s = document.createElement("div");
s.className = "falling-seal";
s.textContent = chars[Math.floor(Math.random() * chars.length)];
s.style.left = Math.random() * 100 + "vw";
s.style.animationDelay = (Math.random() * 1.2) + "s";
s.style.animationDuration = (2.8 + Math.random() * 2) + "s";
s.style.transform = `rotate(${(Math.random() * 30 - 15)}deg)`;
document.body.appendChild(s);
setTimeout(() => s.remove(), 6000);
}
};
const onKey = (e) => {
if (e.key && e.key.length === 1) buf += e.key.toLowerCase();
if (e.key === "你" || e.key === "好") buf += e.key;
if (buf.length > 12) buf = buf.slice(-12);
if (triggers.some((t) => buf.includes(t))) {
fire();
buf = "";
}
clearTimeout(timer);
timer = setTimeout(() => (buf = ""), 2500);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
return null;
};
const ParallaxChar = () => {
// Make the hero background character drift on scroll
React.useEffect(() => {
const el = document.querySelector(".hero-bg-char");
if (!el) return;
const onScroll = () => {
const y = window.scrollY;
el.style.transform = `translateY(${y * -0.25}px) scale(${1 + y * 0.0006})`;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return null;
};
// JS fallback for scroll-progress where animation-timeline unsupported
const ScrollProgressHook = () => {
React.useEffect(() => {
if (CSS.supports && CSS.supports("animation-timeline", "scroll()")) return;
const bar = document.querySelector(".scroll-progress");
if (!bar) return;
const onScroll = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
const p = max > 0 ? h.scrollTop / max : 0;
bar.style.setProperty("--p", p.toFixed(4));
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return null;
};
// Magnetic CTA — pulls the button toward the cursor within range.
const MagneticHooks = () => {
React.useEffect(() => {
const targets = Array.from(document.querySelectorAll(".btn-seal"));
targets.forEach((el) => el.classList.add("magnetic"));
const R = 90;
const onMove = (e) => {
targets.forEach((el) => {
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = e.clientX - cx;
const dy = e.clientY - cy;
const d = Math.hypot(dx, dy);
if (d < R) {
el.style.setProperty("--mx", `${dx * 0.28}px`);
el.style.setProperty("--my", `${dy * 0.28}px`);
el.classList.add("pulled");
} else {
el.style.setProperty("--mx", "0px");
el.style.setProperty("--my", "0px");
el.classList.remove("pulled");
}
});
};
window.addEventListener("pointermove", onMove);
return () => window.removeEventListener("pointermove", onMove);
}, []);
return null;
};
// View-timeline fallback: tag cards as reveal-up; IO toggles .in on non-supporting browsers
const RevealUpHooks = () => {
React.useEffect(() => {
document.querySelectorAll(".entry-card, .cat-tile, .testi-card, .contrib-card, .price-card")
.forEach((el) => el.classList.add("reveal-up"));
if (CSS.supports && CSS.supports("animation-timeline", "view()")) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add("in"); });
}, { threshold: 0.15 });
document.querySelectorAll(".reveal-up").forEach((el) => obs.observe(el));
return () => obs.disconnect();
}, []);
return null;
};
const App = () => (
);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render();