// app.jsx — Root with simple router, state, and Tweaks panel const FONTS = { "Montserrat": "'Montserrat', 'Helvetica Neue', Arial, ui-sans-serif, system-ui, sans-serif", "Manrope": "'Manrope', ui-sans-serif, system-ui, sans-serif", "Plus Jakarta Sans": "'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif", "IBM Plex Sans": "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif", "Space Grotesk": "'Space Grotesk', ui-sans-serif, system-ui, sans-serif", }; const BRAND_PALETTES = { "#F39200": { deep: "#D97A00", soft: "#FFF4E0" }, // KiwiEducation orange (default) "#1F8FB6": { deep: "#176B89", soft: "#E6F3F9" }, // KiwiEducation accent teal "#1d4ed8": { deep: "#1e3a8a", soft: "#eef2ff" }, // royal blue "#0d6e6e": { deep: "#0a4f4f", soft: "#e6f4f4" }, // teal/forest "#7c3aed": { deep: "#5b21b6", soft: "#f3edff" }, // violet "#0f172a": { deep: "#020617", soft: "#f1f5f9" }, // graphite }; const SESSION_KEY = "kiwi_session_token"; const SESSION_EMAIL_KEY = "kiwi_session_email"; function App() { const [t, setTweak] = useTweaks(window.TWEAK_DEFAULTS); const [page, setPage] = React.useState("payment"); const [authReady, setAuthReady] = React.useState(false); const [devMode, setDevMode] = React.useState(null); // null = unknown until /api/config responds // Lead data comes from the external website (URL params / token). // Prefilled with demo data so the funnel is visible without backend. const [formData, setFormData] = React.useState({ firstName: "", lastName: "", email: "", emailVerified: false, phone: "+7 999 123 45 67", citizenship: "Россия", currentCountry: "Сербия", destination: "Новая Зеландия", service: "Иммиграция", englishLevel: "Upper-Intermediate (B2)", industry: "IT и разработка", }); const [intakeData, setIntakeData] = React.useState({}); const [booking, setBooking] = React.useState({ date: null, time: null }); const [hasBooked, setHasBooked] = React.useState(false); const navigate = (p) => { setPage(p); window.scrollTo({ top: 0, behavior: "instant" }); }; React.useEffect(() => { fetch("/api/config") .then(r => r.json()) .then(d => { setDevMode(d.dev_mode === true); }) .catch(() => { setDevMode(false); }); // fail-safe: treat as prod if API unreachable }, []); // Listen for navigation messages from Responsive Preview host React.useEffect(() => { const onMsg = (e) => { if (e.data && e.data.type === "__rp_navigate" && e.data.page) { setPage(e.data.page); } }; window.addEventListener("message", onMsg); return () => window.removeEventListener("message", onMsg); }, []); // Apply tweaks → CSS variables on :root + body class React.useEffect(() => { const r = document.documentElement; r.style.setProperty("--font", FONTS[t.font] || FONTS.Montserrat); const pal = BRAND_PALETTES[t.brand] || BRAND_PALETTES["#F39200"]; r.style.setProperty("--brand", t.brand); r.style.setProperty("--brand-deep", pal.deep); r.style.setProperty("--brand-soft", pal.soft); r.style.setProperty("--radius", `${t.roundedness}px`); r.style.setProperty("--radius-sm", `${Math.max(6, t.roundedness - 4)}px`); document.body.classList.remove("bg-tint","bg-warm","bg-cool"); if (t.background === "tint") document.body.classList.add("bg-tint"); if (t.background === "warm") document.body.classList.add("bg-warm"); if (t.background === "cool") document.body.classList.add("bg-cool"); if (t.density === "compact") { r.style.setProperty("--density-y", "10px"); } else if (t.density === "comfy") { r.style.setProperty("--density-y", "18px"); } else { r.style.setProperty("--density-y", "14px"); } }, [t]); const _loadCustomerState = (sessionToken) => fetch("/api/customer/me", { headers: { Authorization: `Bearer ${sessionToken}` } }) .then(r => r.ok ? r.json() : null) .then(d => { if (d) { setHasBooked(d.hasBooked); if (d.name) setFormData(prev => ({ ...prev, customerName: d.name })); } }) .catch(() => {}); React.useEffect(() => { const params = new URLSearchParams(window.location.search); const urlToken = params.get("token"); if (urlToken) { window.history.replaceState({}, "", window.location.pathname); fetch("/api/auth/activate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: urlToken }), }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(data => { localStorage.setItem(SESSION_KEY, data.session_token); localStorage.setItem(SESSION_EMAIL_KEY, data.email); setFormData(prev => ({ ...prev, email: data.email })); setPage("setup"); setAuthReady(true); _loadCustomerState(data.session_token); }) .catch(() => { window.location.href = "/request-access.html?expired=1"; }); return; } const storedToken = localStorage.getItem(SESSION_KEY); if (storedToken) { fetch(`/api/auth/verify?session_token=${encodeURIComponent(storedToken)}`) .then(r => r.json()) .then(data => { if (data.valid) { localStorage.setItem(SESSION_EMAIL_KEY, data.email); setFormData(prev => ({ ...prev, email: data.email })); setPage("setup"); _loadCustomerState(storedToken); } else { localStorage.removeItem(SESSION_KEY); localStorage.removeItem(SESSION_EMAIL_KEY); } setAuthReady(true); }) .catch(() => setAuthReady(true)); return; } setAuthReady(true); }, []); const shared = { navigate, formData, setFormData, intakeData, setIntakeData, booking, setBooking, tweaks: t, devMode, hasBooked }; return ( <> {page === "payment" && } {page === "setup" && authReady && (() => { if (!localStorage.getItem(SESSION_KEY)) { window.location.href = "/request-access.html"; return null; } return ; })()} {page === "confirmation" && } {/* Page switcher dot — small floating control so you can jump between pages */} {devMode && } {devMode && setTweak('brand', v)}/> setTweak('font', v)}/> setTweak('background', v)}/> setTweak('density', v)}/> setTweak('roundedness', v)}/> setTweak('showProgress', v)}/> navigate(v)}/> } ); } function PageSwitcher({ current, navigate }) { const [hover, setHover] = React.useState(false); const PAGES = [ { id: "payment", label: "1 · Оплата" }, { id: "setup", label: "2 · Подготовка" }, { id: "confirmation", label: "3 · Подтверждение" }, ]; return (
setHover(true)} onMouseLeave={()=>setHover(false)} style={{ position:"fixed", left:16, bottom:16, zIndex:9998, display:"flex", gap:4, padding:6, background:"rgba(255,255,255,.85)", border:"1px solid var(--line)", borderRadius:999, backdropFilter:"blur(10px) saturate(140%)", WebkitBackdropFilter:"blur(10px) saturate(140%)", boxShadow:"0 4px 20px rgba(15,23,42,.1)", fontSize:12, fontFamily:"ui-sans-serif, system-ui", }}> {PAGES.map(p => ( ))}
); } ReactDOM.createRoot(document.getElementById('root')).render();