// App.jsx — Ozimiz landing page
// Structure: TopNav → Hero (with product peek card) → Proof strip → Features
// → Dual-persona row → Locales/currencies → Pricing → FAQ → Final CTA → Footer
const APP_ORIGIN = "https://app.ozimiz.net";
const LANG_MAP = { ru: "ru", kk: "kk-Cyrl", en: "en" };
function appUrl(lang, path) {
const l = LANG_MAP[lang] || "ru";
return APP_ORIGIN + "/" + l + (path || "");
}
Object.assign(window, { appUrl });
function useI18n() {
const [lang, setLang] = React.useState(() => {
try { return localStorage.getItem("ozimiz.lang") || "ru"; } catch(_) { return "ru"; }
});
const [dark, setDark] = React.useState(() => {
try { return localStorage.getItem("ozimiz.theme") === "dark"; } catch(_) { return false; }
});
React.useEffect(() => {
try { localStorage.setItem("ozimiz.lang", lang); } catch(_) {}
document.documentElement.setAttribute("lang", lang === "kk" ? "kk" : lang);
}, [lang]);
React.useEffect(() => {
try { localStorage.setItem("ozimiz.theme", dark ? "dark" : "light"); } catch(_) {}
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
}, [dark]);
const t = window.I18N[lang] || window.I18N.ru;
return { lang, setLang, dark, setDark, t };
}
// Cross-origin session probe: if the visitor is already signed in at
// app.ozimiz.net, swap the "sign in / start" CTAs for an "open app" link.
function useAppSession() {
const [user, setUser] = React.useState(null);
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
let cancelled = false;
fetch(APP_ORIGIN + "/api/auth/get-session", { credentials: "include" })
.then(r => r.ok ? r.json() : null)
.then(data => { if (!cancelled) setUser(data && data.user ? data.user : null); })
.catch(() => undefined)
.finally(() => { if (!cancelled) setReady(true); });
return () => { cancelled = true; };
}, []);
return { user, ready };
}
Object.assign(window, { useAppSession });
function TopNav({ lang, setLang, dark, setDark, t }) {
const { user, ready } = useAppSession();
const [langOpen, setLangOpen] = React.useState(false);
const langCodes = ["ru", "kk", "en"];
const linkStyle = {
font: "500 14px Manrope, sans-serif", color: "var(--fg-muted)",
padding: "8px 12px", borderRadius: 8, textDecoration: "none",
transition: "color var(--dur-base) var(--ease-standard), background var(--dur-base) var(--ease-standard)",
};
return (
{langOpen && (
setLangOpen(false)} style={{
position: "absolute", top: "calc(100% + 6px)", right: 0,
background: "var(--surface-raised)", border: "1px solid var(--border)",
borderRadius: 10, boxShadow: "var(--shadow-md)",
padding: 4, minWidth: 160, zIndex: 50,
}}>
{langCodes.map(code => (
))}
)}
{ready && user ? (
{t.nav.openApp}
) : (
<>
{t.nav.signIn}
{t.nav.start}
>
)}
);
}
function Hero({ t, lang }) {
return (
{t.hero.eyebrow}
{t.hero.title}
{t.hero.sub}
{t.hero.foot}
);
}
// A miniature of the actual product — pulled from the member kit's vocabulary.
function HeroPeek({ t }) {
return (
{/* Backplate */}
{/* Feed post */}
Айдана Жұмабекова
Модератор · 2ч
Закреп
Добавила модуль «Портфель для новичка». Вопросы — в комментариях, отвечу до вечера.
34
12
{/* Classroom module */}
Модуль 2 · Портфель для новичка
4 урока · 38 мин
2 / 4
{[
{ t: "Во что инвестировать первые ₸100 000", m: "7 мин", done: true },
{ t: "ETF на KASE: полный разбор", m: "12 мин", done: true, current: false },
{ t: "Ребалансировка раз в квартал", m: "9 мин", done: false, current: true },
{ t: "Как не сливать на панике", m: "10 мин", done: false, locked: true },
].map((row, i) => (
{row.done && }
{row.m}
{row.locked &&
}
))}
{/* Payment row */}
Оплата прошла · ioka.kz
Kaspi Gold · ··7821
24{"\u202F"}900{"\u00A0"}₸
);
}
const peekCard = {
background: "var(--surface-raised)",
border: "1px solid var(--border)",
borderRadius: 14,
padding: "14px 16px",
boxShadow: "var(--shadow-sm)",
};
// --- Button tokens --- //
const primaryBtn = {
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8,
padding: "10px 16px",
background: "var(--brand-primary)", color: "var(--brand-primary-fg)",
border: 0, borderRadius: 10,
font: "600 14px Manrope, sans-serif",
textDecoration: "none", cursor: "pointer",
transition: "background var(--dur-base) var(--ease-standard)",
};
const secondaryBtn = {
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8,
padding: "10px 16px",
background: "var(--surface-raised)", color: "var(--fg)",
border: "1px solid var(--border)", borderRadius: 10,
font: "600 14px Manrope, sans-serif",
textDecoration: "none", cursor: "pointer",
transition: "background var(--dur-base) var(--ease-standard)",
};
const ghostBtn = {
display: "inline-flex", alignItems: "center", gap: 6,
padding: "8px 10px",
background: "transparent", color: "var(--fg-muted)",
border: "1px solid var(--border)", borderRadius: 8,
font: "500 13px Manrope, sans-serif", cursor: "pointer",
transition: "background var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard)",
};
Object.assign(window, { useI18n, TopNav, Hero, primaryBtn, secondaryBtn, ghostBtn, peekCard });