// App.jsx — main site component const { useState, useEffect, useRef } = React; const DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "sage", "fontScheme": "editorial", "heroLayout": "split", "grain": true }/*EDITMODE-END*/; // Each palette also exposes *Rgb triplets so alpha-tinted bits (nav-scroll // backdrop, pulse rings, gradient washes) follow the active theme via // rgba(var(--bgRgb), ) etc. const ACCENTS = { sage: { bg: "#f6f1e8", // warm cream bgRgb: "246, 241, 232", bgAlt: "#efe6d5", // deeper cream ink: "#2a2620", // warm near-black inkSoft: "#6b6357", accent: "#8a9a7b", // sage green accentRgb: "138, 154, 123", accentDeep: "#5e6e50", accentSoft: "#d9dfcf", card: "#fbf8f1", cardRgb: "251, 248, 241", border: "#e4dccb", }, terracotta: { bg: "#f7f0e7", bgRgb: "247, 240, 231", bgAlt: "#f0e4d2", ink: "#2b221c", inkSoft: "#6d6054", accent: "#c07a5e", accentRgb: "192, 122, 94", accentDeep: "#9a5a42", accentSoft: "#eed9ca", card: "#fcf8f1", cardRgb: "252, 248, 241", border: "#e6dccc", }, rose: { bg: "#f8f1ec", bgRgb: "248, 241, 236", bgAlt: "#f1e4db", ink: "#2a231f", inkSoft: "#6d6056", accent: "#b98878", accentRgb: "185, 136, 120", accentDeep: "#8e6150", accentSoft: "#ecd9cf", card: "#fcf7f3", cardRgb: "252, 247, 243", border: "#e8dccf", }, }; const FONTS = { editorial: { label: "Editorial — Fraunces + Inter", display: "'Fraunces', 'Times New Roman', serif", body: "'Inter', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 400, displayTracking: "-0.02em", }, modern: { label: "Modern — Instrument + Geist", display: "'Instrument Serif', serif", body: "'Geist', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 400, displayTracking: "-0.015em", }, classic: { label: "Classic — Cormorant + Work Sans", display: "'Cormorant Garamond', serif", body: "'Work Sans', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 500, displayTracking: "-0.01em", }, parisian: { label: "Parisian — Playfair + DM Sans", display: "'Playfair Display', serif", body: "'DM Sans', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 500, displayTracking: "-0.02em", }, warm: { label: "Warm — DM Serif + Manrope", display: "'DM Serif Display', serif", body: "'Manrope', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 400, displayTracking: "-0.025em", }, bookish: { label: "Bookish — EB Garamond + Nunito", display: "'EB Garamond', serif", body: "'Nunito', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 500, displayTracking: "-0.005em", }, bold: { label: "Bold sans — Bricolage + Inter", display: "'Bricolage Grotesque', sans-serif", body: "'Inter', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 600, displayTracking: "-0.035em", }, soft: { label: "Soft — Lora + Outfit", display: "'Lora', serif", body: "'Outfit', system-ui, sans-serif", mono: "'JetBrains Mono', monospace", displayWeight: 500, displayTracking: "-0.015em", }, }; // EUR/GBP rate (~1.17) — prices already computed in PRICES, just format function fmt(amount, currency) { const symbol = currency === "gbp" ? "£" : "€"; return `${symbol}${amount}`; } function useEdit() { const [tweaks, setTweaks] = useTweaks(DEFAULTS); return [tweaks, setTweaks]; } function App() { const [tweaks, setTweaks] = useTweaks(DEFAULTS); const [lang, setLang] = useState("ru"); const [currency, setCurrency] = useState("gbp"); const t = window.I18N[lang]; const P = window.PRICES; const palette = ACCENTS[tweaks.accent] || ACCENTS.sage; const fonts = FONTS[tweaks.fontScheme] || FONTS.editorial; // Apply CSS vars useEffect(() => { const r = document.documentElement; Object.entries(palette).forEach(([k, v]) => r.style.setProperty(`--${k}`, v)); r.style.setProperty("--font-display", fonts.display); r.style.setProperty("--font-body", fonts.body); r.style.setProperty("--font-mono", fonts.mono); r.style.setProperty("--font-display-weight", String(fonts.displayWeight)); r.style.setProperty("--font-display-tracking", fonts.displayTracking); }, [palette, fonts]); // Sync with the selected language so screen readers + search // engines see the right locale. useEffect(() => { document.documentElement.lang = lang === "ua" ? "uk" : "ru"; }, [lang]); return (
{tweaks.grain && ); } function NavBar({ t, lang, setLang, currency, setCurrency }) { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { const on = () => setScrolled(window.scrollY > 20); window.addEventListener("scroll", on, { passive: true }); on(); return () => window.removeEventListener("scroll", on); }, []); // Close mobile menu on Esc and lock body scroll while it's open. useEffect(() => { if (!menuOpen) return; const onKey = (e) => { if (e.key === "Escape") setMenuOpen(false); }; window.addEventListener("keydown", onKey); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prev; }; }, [menuOpen]); const close = () => setMenuOpen(false); const links = [ { href: "#about", label: t.nav.about }, { href: "#pricing", label: t.nav.pricing }, { href: "#how", label: t.nav.courses }, { href: "#resources", label: t.nav.resources }, { href: "#reviews", label: t.nav.reviews }, { href: "#faq", label: t.nav.faq }, ]; return (
A Alina · English
{t.nav.contact}
{menuOpen && (
{t.nav.contact}
)}
); } function Hero({ t, layout }) { return (
{t.hero.kicker}

{t.hero.title1} {t.hero.title2} {t.hero.title3}

{t.hero.sub}

{t.hero.cta}
{t.hero.ctaSub}
Alina
Available · new students
01 Your tutor, Alina
); } function Badge({ label }) { return ( {label} ); } function About({ t }) { return (
Alina with English File textbook
{t.about.kicker}

{t.about.title}

{t.about.p1}

{t.about.p2}

{t.about.p3}

{t.about.factsTitle}
{t.about.levels.map((lv, i) => ( {lv} ))}
— Alina
); } function Pricing({ t, currency, setCurrency, prices }) { const c = currency; const price = (k) => prices[k][c]; const hourly = c === "gbp" ? "£13" : "€15"; const pack4Per = c === "gbp" ? "£11.25" : "€13.25"; const pack8Per = c === "gbp" ? "£10.75" : "€12.60"; return (
{t.pricing.kicker}

{t.pricing.title}

{t.pricing.sub}

{t.pricing.pickerLabel}
); } function PriceCard({ variant, eyebrow, title, price, unit, subPrice, save, desc, bullets, ctaLabel, t, }) { return (
{eyebrow &&
{eyebrow}
}

{title}

{price} {unit && {unit}} {save && {save}}
{subPrice &&
{subPrice}
}

{desc}

    {bullets.map((b, i) => (
  • {b}
  • ))}
{ctaLabel}
); } function HowItWorks({ t }) { return (
{t.how.kicker}

{t.how.title}

{t.how.steps.map((s, i) => (
{s.n}

{s.t}

{s.d}

))}
); } function Reviews({ t }) { const trackRef = useRef(null); const [canPrev, setCanPrev] = useState(false); const [canNext, setCanNext] = useState(true); // Recompute arrow availability whenever the track scrolls or the viewport // resizes (which can change clientWidth and shift edges). useEffect(() => { const el = trackRef.current; if (!el) return; const update = () => { const max = el.scrollWidth - el.clientWidth; setCanPrev(el.scrollLeft > 4); setCanNext(el.scrollLeft < max - 4); }; update(); el.addEventListener("scroll", update, { passive: true }); window.addEventListener("resize", update); return () => { el.removeEventListener("scroll", update); window.removeEventListener("resize", update); }; }, [t.reviews.items.length]); const nudge = (dir) => { const el = trackRef.current; if (!el) return; const card = el.querySelector(".review"); const gap = 20; const step = card ? card.offsetWidth + gap : el.clientWidth * 0.85; el.scrollBy({ left: dir * step, behavior: "smooth" }); }; return (
{t.reviews.kicker}

{t.reviews.title}

{t.reviews.items.map((r, i) => (
{r.q}
{r.a} {r.role}
))}
); } function Resources({ t }) { const r = t.resources; return (
{r.kicker}
{r.free}

{r.title}

{r.line1}
{r.line2}
{r.tgHandle} {r.tgCta}
); } function FAQ({ t }) { const [open, setOpen] = useState(0); return (
{t.faq.kicker}

{t.faq.title}

{t.faq.items.map((q, i) => (
{open === i &&
{q.a}
}
))}
); } const TG_USERNAME = "alinakobzareng"; const TG_URL = `https://t.me/${TG_USERNAME}`; function TelegramGlyph({ size = 22 }) { return ( ); } function Contact({ t }) { const [sent, setSent] = useState(false); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); const cleanup = useRef(null); // Build a tidy message body that the user can paste into Telegram. const composeMessage = () => { const lines = []; if (name.trim()) lines.push(`${t.contact.name}: ${name.trim()}`); if (email.trim()) lines.push(`${t.contact.email}: ${email.trim()}`); if (message.trim()) { if (lines.length) lines.push(""); lines.push(message.trim()); } return lines.join("\n"); }; const handleSubmit = async (e) => { e.preventDefault(); const body = composeMessage(); // Try modern clipboard API; fall back silently if unavailable. try { if (body && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(body); } } catch (_) { /* clipboard blocked — proceed anyway */ } window.open(TG_URL, "_blank", "noopener,noreferrer"); setSent(true); if (cleanup.current) clearTimeout(cleanup.current); cleanup.current = setTimeout(() => setSent(false), 12000); }; return (
{t.contact.kicker}

{t.contact.title}

{t.contact.sub}

{t.contact.tgLead} {t.contact.tgHandle} {t.contact.tgCta}

{t.contact.replyTime}

{t.contact.formIntro}

{sent ? (

{t.contact.sentTitle}

{t.contact.sentBody}

{t.contact.sentCta}
) : (