/* global React */ // Shared UI components: Header, Footer, LanguageSwitcher, ProgramCard, Icon, FormField, FileDrop const { useState, useEffect, useRef } = React; // Tiny icon set drawn as inline SVG (kept generic + minimal per brand "premium minimalism") function Arrow({ size = 16 }) { return ( ); } function ArrowLeft({ size = 16 }) { return ( ); } function Check({ size = 24 }) { return ( ); } function UploadIcon() { return ( ); } function InstagramIcon({ size = 16 }) { return ( ); } function LanguageSwitcher({ lang, setLang }) { return (
{["en", "es", "ru"].map((code) => ( ))}
); } // Mobile-only compact language picker. Default is whatever lang prop is set to // (English by default elsewhere). Shows current code + a chevron; tap reveals // the other two options in a small dropdown panel. Closes on outside click, // Escape, or after picking an option. function LanguageDropdown({ lang, setLang }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; const onKey = (e) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); }; }, [open]); const others = ["en", "es", "ru"].filter((c) => c !== lang); return (
{open && ( )}
); } function Header({ lang, setLang, navigate, onDark, route }) { const t = window.I18N[lang]; // Smart nav: on home page, scroll to the section; elsewhere, navigate to the dedicated page. const goPrograms = () => { if (route && route.name === "home") { const el = document.getElementById("programs"); if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: "smooth" }); } else { navigate({ name: "home" }); setTimeout(() => { const el = document.getElementById("programs"); if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: "smooth" }); }, 60); } }; // "Our cases" lives as an in-page section between Programs and Partners on the home page. // From any inner page we navigate home first, then scroll once the section mounts. const goCases = () => { if (route && route.name === "home") { const el = document.getElementById("cases"); if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: "smooth" }); } else { navigate({ name: "home" }); setTimeout(() => { const el = document.getElementById("cases"); if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: "smooth" }); }, 60); } }; const isActive = (n) => route && route.name === n; // Brand click: always returns the user to the very top of the home page, // whether they're on home (where navigate() wouldn't trigger a hashchange) // or on any inner page. const goHomeTop = () => { if (route && route.name === "home") { window.scrollTo({ top: 0, behavior: "smooth" }); } else { navigate({ name: "home" }); setTimeout(() => window.scrollTo({ top: 0, behavior: "auto" }), 60); } }; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); goHomeTop(); } }}> FirstSteps
FirstSteps Football placement
{/* Desktop: inline 3-pill switcher. Mobile (<=980px): collapsed dropdown — CSS toggles which one is visible. */}
); } function Footer({ lang, navigate }) { const t = window.I18N[lang].footer; const programs = window.PROGRAMS; return ( ); } function ProgramCard({ program, idx, lang, navigate }) { const t = window.I18N[lang]; const price = window.PROGRAM_PRICES && window.PROGRAM_PRICES[program.id] ? window.PROGRAM_PRICES[program.id][lang] : null; return (
navigate({ name: "program", id: program.id })} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") navigate({ name: "program", id: program.id }); }} >
0{idx + 1} / 06

{program.title[lang]}

{program.short[lang]}

{program.duration[lang]} {program.location[lang]}
{price && (
{lang === "ru" ? "Стоимость" : (lang === "es" ? "Precio" : "Price")} {price}
)}
); } function Field({ label, required, error, hint, children }) { return (
{children} {error && {error}} {!error && hint && {hint}}
); } function FileDrop({ lang, file, setFile }) { const t = window.I18N[lang].form; const inputRef = useRef(null); return (
inputRef.current && inputRef.current.click()} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const f = e.dataTransfer.files && e.dataTransfer.files[0]; if (f) setFile(f); }} role="button" tabIndex={0} >
{file ? file.name : t.cvDrop}
{file ? Math.round(file.size / 1024) + " KB" : t.cvHint}
setFile(e.target.files && e.target.files[0])} />
); } // Intro splash — shown on first home view per session. Logo animates: "f1rst" fades up, // then "steps" travels from bottom-left to its final position above the "t". function IntroSplash({ lang, onDone }) { const tagline = { en: "Your path to professional football", es: "Tu camino al fútbol profesional", ru: "Твой путь в профессиональный футбол", }[lang] || "Your path to professional football"; const [leaving, setLeaving] = useState(false); useEffect(() => { const leaveAt = setTimeout(() => setLeaving(true), 3000); const removeAt = setTimeout(onDone, 3600); return () => { clearTimeout(leaveAt); clearTimeout(removeAt); }; }, [onDone]); const skip = () => { setLeaving(true); setTimeout(onDone, 450); }; return (
f1rst steps
{tagline}
); } // ---------- VIDEO CAROUSEL ---------- // Auto-playing, muted, looping carousel for short MP4s. Used on the home page // "Our cases" section and on the full-year program "Program overview" section. // Controls per active card: play/pause, sound, expand (modal on desktop, native // fullscreen on small viewports), plus a click-to-seek scrubber with timecode. function VideoCarousel({ videos, labels, className }) { // videos: [{ src, title, subtitle? }] const [active, setActive] = useState(0); const [muted, setMuted] = useState(true); const [playing, setPlaying] = useState(true); const [progress, setProgress] = useState(0); // 0..1 const [duration, setDuration] = useState(0); const [currentTime, setCurrentTime] = useState(0); const [expanded, setExpanded] = useState(false); const refs = useRef([]); const expandedRef = useRef(null); const scrubRef = useRef(null); const total = videos.length; const go = (delta) => setActive((i) => (i + delta + total) % total); // Only the active card plays — others pause and rewind so they don't waste // bandwidth or jump audibly when the user steps through them. Switching slides // also resets the play/progress state so the new clip starts cleanly. useEffect(() => { refs.current.forEach((v, i) => { if (!v) return; v.muted = muted; if (i === active) { try { v.currentTime = 0; } catch (e) {} if (playing) { const p = v.play(); if (p && p.catch) p.catch(() => {}); } else { v.pause(); } } else { v.pause(); try { v.currentTime = 0; } catch (e) {} } }); setProgress(0); setCurrentTime(0); }, [active]); // eslint-disable-line react-hooks/exhaustive-deps // Honour play/pause + mute toggles on the active video without resetting time. useEffect(() => { const v = refs.current[active]; if (!v) return; v.muted = muted; if (playing) { const p = v.play(); if (p && p.catch) p.catch(() => {}); } else { v.pause(); } }, [playing, muted, active]); // Mobile gets native fullscreen (browser/OS chrome). Desktop gets a contained // overlay so the page stays visible around it. In BOTH cases we pause the // inline video while the expanded copy is active so its audio isn't duplicated. const openExpanded = () => { const inline = refs.current[active]; if (inline) { try { inline.pause(); } catch (e) {} } const isMobile = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(max-width: 720px)").matches; if (isMobile) { if (inline) { const req = inline.requestFullscreen || inline.webkitEnterFullscreen || inline.webkitRequestFullscreen; if (req) { // On mobile the same