// LoreMaster landing — main app
const { useState, useEffect, useRef, useMemo } = React;
// ── Inline icons for features / footer ──────────────────────
const Icon = {
Network: () =>
,
Import: () =>
,
Sparkle: () =>
,
Chat: () =>
,
Mail: () =>
,
Arrow: () =>
,
Check: () =>
};
const FEATURE_ICONS = [Icon.Network, Icon.Import, Icon.Sparkle, Icon.Chat];
// ── Hook: scroll parallax ─────────────────────────────────
function useScroll() {
const [y, setY] = useState(0);
useEffect(() => {
let raf = null;
const onScroll = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
setY(window.scrollY);
raf = null;
});
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
return y;
}
// ── NAV ───────────────────────────────────────────────────
function Nav({ lang, setLang, copy }) {
return (
);
}
// ── HERO ──────────────────────────────────────────────────
function Hero({ lang, copy, scrollY, parallaxK }) {
const p = parallaxK;
const castleY = Math.min(scrollY * 0.4 * p, 400);
const orbY1 = scrollY * 0.2 * p;
const orbY2 = scrollY * 0.35 * p;
const copyY = scrollY * -0.05 * p;
const stageOffset = scrollY * 0.07 * p;
return (
{copy.hero.badge}
{copy.hero.titleA}
{copy.hero.titleB}
{copy.hero.sub}
);
}
// ── FEATURES ──────────────────────────────────────────────
function Features({ copy }) {
return (
{copy.features.eyebrow}
{copy.features.titleA}{" "}
{copy.features.titleB}
{copy.features.sub}
{copy.features.cards.map((c, i) => {
const I = FEATURE_ICONS[i];
return (
);
})}
);
}
// ── SHOWCASE — sticky scene panel (slide content only, no iPhone), driven by scroll
function ShowcasePanel({ step, copy }) {
if (step === 0) {
const w = copy.iphone.worlds;
return (
{w[0].desc}
{w[0].tags.map(([e, t], j) => (
{e}{t}
))}
{w[1].desc}
{w[1].tags.map(([e, t], j) => (
{e}{t}
))}
);
}
if (step === 1) {
const r = copy.iphone.region;
return (
{r.labels.government}{r.government.icon}{r.government.value}
{r.labels.population}{r.population.icon}{r.population.value}
{r.labels.capital}{r.capital.icon}{r.capital.value}
{r.bubble}
);
}
if (step === 2) {
const a = copy.iphone.ai;
return (
{a.userMsg}
{a.leadPre}{a.leadChip}
{a.ready}
{a.diff.map((d, i) => (
{d.k}:
{d.v}
))}
);
}
const r = copy.iphone.rp;
return (
📜
{r.quest.label}
{r.quest.title}
{r.dialogue}
);
}
// ── SHOWCASE — compact 4-up horizontal step cards
function Showcase({ copy }) {
const ref = useRef(null);
useEffect(() => {
if (!ref.current) return;
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add("in");
});
}, { threshold: 0.2, rootMargin: "0px 0px -10% 0px" });
ref.current.querySelectorAll(".sc-step").forEach(el => io.observe(el));
return () => io.disconnect();
}, []);
return (
{copy.showcase.eyebrow}
{copy.showcase.titleA}
{copy.showcase.titleEm}
{copy.showcase.steps.map((s, i) => (
))}
);
}
// ── HOW IT WORKS ──────────────────────────────────────────
function HowItWorks({ copy }) {
return (
{copy.how.eyebrow}
{copy.how.titleA}
{copy.how.titleEm}
{copy.how.steps.map((s, i) =>
{["01", "02", "03"][i]}
{s.t}
{s.d}
)}
);
}
// ── WAITLIST FORM ─────────────────────────────────────────
function Waitlist({ copy }) {
const [submitted, setSubmitted] = useState(false);
const L = copy.waitlist.labels;
function handleSubmit(e) {
e.preventDefault();
setSubmitted(true);
}
return (
{copy.waitlist.eyebrow}
{copy.waitlist.titleA}
{copy.waitlist.titleEm}
{copy.waitlist.sub}
{copy.waitlist.statsLabel.map((l, i) =>
{copy.waitlist.statsValue[i]}
{l}
)}
{!submitted ?
:
{copy.waitlist.success.title}
{copy.waitlist.success.body}
}
);
}
// ── NEWSLETTER STRIP ──────────────────────────────────────
function Newsletter({ copy }) {
const [done, setDone] = useState(false);
return (
{copy.newsletter.label}
{copy.newsletter.body}
{!done ?
:
{copy.waitlist.success.title}
}
);
}
// ── FAQ ───────────────────────────────────────────────────
function FAQ({ copy }) {
return (
{copy.faq.items.map((it, i) =>
{it.q}
{it.a}
)}
);
}
// ── FOOTER ────────────────────────────────────────────────
function Footer({ copy }) {
return (
);
}
// ── Tweaks defaults ───────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"parallax": 100
} /*EDITMODE-END*/;
// ── ROOT ──────────────────────────────────────────────────
function App() {
const [lang, setLang] = useState(() => {
const saved = localStorage.getItem("lm.lang");
if (saved) return saved;
return (navigator.language || "").toLowerCase().startsWith("ru") ? "ru" : "en";
});
useEffect(() => {localStorage.setItem("lm.lang", lang);}, [lang]);
useEffect(() => {document.documentElement.lang = lang;}, [lang]);
const copy = window.COPY[lang];
const scrollY = useScroll();
const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
const parallaxK = tweaks.parallax / 100;
return (
<>
setTweak("parallax", v)}
min={0} max={200} step={5}
unit="%" />
>);
}
ReactDOM.createRoot(document.getElementById("root")).render();