/* global React */ // FirstSteps — page components: Home, ProgramDetail, Apply, ThankYou const { useState, useMemo, useEffect, useRef, forwardRef, useImperativeHandle } = React; // Canonical English position values — what we send in the payload regardless of UI language. // The localized labels (window.I18N[lang].form.positions) render at the same indices. const POSITION_VALUES = ["Goalkeeper", "Defender", "Midfielder", "Forward", "Other"]; // ----- International phone field (intl-tel-input wrapper) ----- const PhoneInput = forwardRef(function PhoneInput({ lang, onValid }, ref) { const inputRef = useRef(null); const itiRef = useRef(null); useEffect(() => { if (!inputRef.current || !window.intlTelInput) return; // Default country by interface language. EN/ES → Spain, RU → Russia. const initial = lang === "ru" ? "ru" : "es"; itiRef.current = window.intlTelInput(inputRef.current, { initialCountry: initial, preferredCountries: ["es", "ru", "gb", "us", "de", "fr", "it", "pt", "kz", "ua"], separateDialCode: false, autoPlaceholder: "polite", formatOnDisplay: true, utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js", }); return () => { try { itiRef.current && itiRef.current.destroy(); } catch (e) {} itiRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Update default country when interface language changes — only if user hasn't typed yet. useEffect(() => { if (!itiRef.current || !inputRef.current) return; if (inputRef.current.value && inputRef.current.value.trim() !== "") return; itiRef.current.setCountry(lang === "ru" ? "ru" : "es"); }, [lang]); useImperativeHandle(ref, () => ({ getNumber: () => { if (itiRef.current) { const n = itiRef.current.getNumber(); if (n) return n; } return inputRef.current ? inputRef.current.value : ""; }, isValid: () => { if (!itiRef.current || !inputRef.current) return false; const raw = inputRef.current.value || ""; if (raw.trim() === "") return false; try { return !!itiRef.current.isValidNumber(); } catch (e) { return raw.replace(/\D/g, "").length >= 7; } }, getRaw: () => inputRef.current ? inputRef.current.value : "", }), []); return ( onValid && onValid()} /> ); }); // ---------- HOME ---------- // Carousel pool: 4 featured programs (per brief) const FEATURED_IDS = ["full-year-tenerife", "trial", "community", "analysis"]; // "Our cases" — short MP4 clips that play inline in the carousel between the // Programs and Partners sections. Files live in /assets/cases. const CASE_VIDEOS = [ { src: "assets/cases/Sultan_full_year.mp4", title: "Sultan · Full-year program" }, { src: "assets/cases/Ismaila_trials.mp4", title: "Ismaila · Trials" }, { src: "assets/cases/Constantinos_trials.mp4", title: "Constantinos · Trials" }, { src: "assets/cases/Lucas_trials.mp4", title: "Lucas · Trials" }, { src: "assets/cases/Efthimis_trials.mp4", title: "Efthimis · Trials" }, ]; // Program-detail overview videos: only shown on the full-year program page, // directly after the Requirements block. Mix of training footage, parent // testimony and a coach's perspective. const FULL_YEAR_OVERVIEW_VIDEOS = [ { src: "assets/overview/training-overview.mp4", title: "Inside training" }, { src: "assets/overview/parent-comment.mp4", title: "Parent perspective" }, { src: "assets/overview/aris-aristidou.mp4", title: "Mr Aris Aristidou — head coach" }, { src: "assets/cases/Ismaila_trials.mp4", title: "Ismaila · On the pitch" }, { src: "assets/cases/Sultan_full_year.mp4", title: "Sultan · Full-year program" }, ]; function scrollToId(id, offset = 80) { const el = document.getElementById(id); if (el) window.scrollTo({ top: el.offsetTop - offset, behavior: "smooth" }); } function ProgramCarousel({ lang, navigate, programs }) { const t = window.I18N[lang]; const det = t.detail; const [index, setIndex] = useState(0); const total = programs.length; const go = (delta) => setIndex((i) => (i + delta + total) % total); const prev = () => go(-1); const next = () => go(+1); // Auto-advance — runs continuously regardless of hover/focus so the reveal // animation comes from carousel scroll, not from mouse interactions. useEffect(() => { const id = setInterval(() => setIndex((i) => (i + 1) % total), 5500); return () => clearInterval(id); }, [total]); useEffect(() => { const onKey = (e) => { if (e.key === "ArrowLeft") prev(); if (e.key === "ArrowRight") next(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const prevI = (index - 1 + total) % total; const nextI = (index + 1) % total; const prevP = programs[prevI]; const curP = programs[index]; const nextP = programs[nextI]; const SlideCard = ({ p, i, active }) => { const bg = (window.PROGRAM_IMAGES && window.PROGRAM_IMAGES[p.id]) || null; return (
{ if (active) navigate({ name: "program", id: p.id }); else setIndex(i); }} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") { if (active) navigate({ name: "program", id: p.id }); else setIndex(i); } }} aria-label={p.title[lang]} >
{bg &&
); }; return (
{programs.map((p, i) => (
); } function HomePage({ lang, navigate }) { const t = window.I18N[lang]; const featured = FEATURED_IDS.map((id) => window.PROGRAMS.find((p) => p.id === id)).filter(Boolean); return (
{t.hero.eyebrow}

{t.hero.slogan_1}{" "} {t.hero.slogan_2}

{t.programs.eyebrow}

{t.programs.title}

{t.programs.desc}

{window.PROGRAMS.map((p, i) => ( ))}
{t.cases.eyebrow}

{t.cases.title}

{t.cases.desc}

{t.partners.title}
{t.partners.meta}
{window.PARTNERS.map((p) => (
{p.name}
{p.name}
{p.loc}
))}
{t.how.eyebrow}

{t.how.title}

{t.how.steps.map((s, i) => (
0{i + 1}
{s.t}
{s.b}
))}
{t.about.eyebrow}

{t.about.title}

{t.about.body}

{t.about.pillars.map((p, i) => (
{p.k}
{p.v}
))}
{t.collab.eyebrow}

{t.collab.title}

{t.collab.body}

{t.history.eyebrow}

{t.history.title}

{t.history.items.map((h, i) => (
{h.y}
{h.t}
{h.b}
))}
); } // ---------- PROGRAM DETAIL ---------- function TrialAgePicker({ lang, program, navigate }) { const t = window.I18N[lang].detail; const heroImg = (window.PROGRAM_IMAGES && window.PROGRAM_IMAGES[program.id]) || null; return (
navigate({ name: "home" })}> ← {t.back}
{t.tag}

{program.title[lang]}

{program.short[lang]}

{t.pickerTitle}

{t.pickerSub}

{program.tracks.map((tr) => ( ))}
); } function TrackDetail({ lang, program, track, navigate }) { const t = window.I18N[lang].detail; const lblClub = typeof track.club === "string" ? track.club : track.club[lang]; const heroImg = (window.PROGRAM_IMAGES && window.PROGRAM_IMAGES[program.id]) || null; return (
navigate({ name: "program", id: program.id })}> ← {t.backToPicker}
{track.ageBand[lang]} · {lblClub}

{track.title[lang]}

{track.sub[lang]}

{t.overview}

{track.overview[lang].map((p, i) =>

{p}

)}

{t.audience}

    {track.audience[lang].map((item, i) =>
  • {item}
  • )}

{t.requirements}

    {track.requirements[lang].map((item, i) =>
  • {item}
  • )}
); } function ProgramDetail({ lang, programId, trackId, navigate }) { const t = window.I18N[lang].detail; const program = window.PROGRAMS.find((p) => p.id === programId); useEffect(() => { window.scrollTo(0, 0); }, [programId, trackId]); if (!program) { return
Not found.
; } // Trial-style program with tracks → show age picker, then specific track if (program.tracks) { if (trackId) { const track = program.tracks.find((x) => x.id === trackId); if (!track) return
Track not found.
; return ; } return ; } // Standard program detail const heroImg = (window.PROGRAM_IMAGES && window.PROGRAM_IMAGES[program.id]) || null; return (
navigate({ name: "home" })}> ← {t.back}
{t.tag}

{program.title[lang]}

{program.short[lang]}

{t.overview}

{program.overview[lang].map((p, i) =>

{p}

)}

{t.audience}

    {program.audience[lang].map((item, i) =>
  • {item}
  • )}

{t.requirements}

    {program.requirements[lang].map((item, i) =>
  • {item}
  • )}
{program.id === "full-year-tenerife" && (
{window.I18N[lang].cases.eyebrow}

{t.programOverview}

)}
); } // ---------- APPLY FORM ---------- // Single form at #/apply/form. Receives its program/track context (and // optionally a "collab" form-type) from sessionStorage via fsReadApplyContext. function ApplyPage({ lang, navigate }) { // Read context once on mount. We don't react to changes — the form is // entered fresh whenever the user navigates here. const ctx = useMemo(() => { const c = (typeof window.fsReadApplyContext === "function") ? window.fsReadApplyContext() : null; return c || {}; }, []); const formType = ctx.type === "collab" ? "collab" : "player"; const programId = ctx.programId || null; const trackId = ctx.trackId || null; const t = window.I18N[lang].form; // Collaboration form-specific strings (different title/sub/legal copy). const ct = window.I18N[lang].collabForm || null; const program = programId ? window.PROGRAMS.find((p) => p.id === programId) : null; const track = trackId && program && program.tracks ? program.tracks.find((x) => x.id === trackId) : null; const [values, setValues] = useState({ first: "", last: "", email: "", age: "", club: "", position: "", video: "", videoLater: false, // when true, the video field is locked and validation is skipped website: "", // honeypot — must remain empty }); const [errors, setErrors] = useState({}); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(false); const [collabSubmitted, setCollabSubmitted] = useState(false); const phoneRef = useRef(null); // Capture UTM parameters once on mount so they survive in-app navigation. const utm = useMemo(() => { const keys = ["utm_source", "utm_campaign", "utm_medium", "utm_content", "utm_term"]; const out = {}; try { const sp = new URLSearchParams(window.location.search); keys.forEach((k) => { out[k] = sp.get(k) || ""; }); } catch (err) { keys.forEach((k) => { out[k] = ""; }); } return out; }, []); useEffect(() => { window.scrollTo(0, 0); }, []); // Player form requires a program. Collaboration form does not. if (formType === "player" && !program) { // No context (e.g. user opened #/apply/form directly with empty storage). // Send them back home to pick a program. return (

{(t && t.errors && t.errors.noProgram) || "Please choose a program first."}

); } const set = (k) => (e) => setValues({ ...values, [k]: e.target.value }); const validate = () => { const e = {}; const errs = formType === "collab" ? (ct && ct.errors) || t.errors : t.errors; if (!values.first.trim()) e.first = errs.first; if (!values.last.trim()) e.last = errs.last; if (!phoneRef.current || !phoneRef.current.isValid()) e.phone = errs.phone; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) e.email = errs.email; // Player-only fields. if (formType === "player") { const a = parseInt(values.age, 10); if (!a || a < 4 || a > 25) e.age = t.errors.age; // Video link is required UNLESS the applicant checked "I'll send later". if (!values.videoLater) { if (!values.video.trim()) e.video = t.errors.video; else if (!/^https:\/\/\S+\.\S+/i.test(values.video.trim())) e.videoFormat = t.errors.videoFormat; } } return e; }; const submit = async (e) => { e.preventDefault(); if (submitting) return; // Honeypot: if a bot filled this hidden field, silently ignore. if (values.website && values.website.trim() !== "") { return; } const errs = validate(); setErrors(errs); if (Object.keys(errs).length > 0) { const firstErrField = document.querySelector(".field.error input, .field.error select"); if (firstErrField) firstErrField.focus(); return; } setSubmitError(false); setSubmitting(true); const programTitle = formType === "collab" ? "Collaboration" : (track ? (track.title && track.title.en) || (program.title && program.title.en) || program.id : (program.title && program.title.en) || program.id); const phoneE164 = phoneRef.current ? phoneRef.current.getNumber() : ""; const payload = { first_name: values.first.trim(), last_name: values.last.trim(), phone: phoneE164, email: values.email.trim(), age: formType === "collab" ? "" : parseInt(values.age, 10), current_club: formType === "collab" ? "" : values.club.trim(), position: formType === "collab" ? "" : values.position, video_link: formType === "collab" ? "" : (values.videoLater ? "Will send later" : values.video.trim()), program: programTitle, language: (lang || "en").toUpperCase(), utm_source: utm.utm_source || "", utm_campaign: utm.utm_campaign || "", utm_medium: utm.utm_medium || "", utm_content: utm.utm_content || "", utm_term: utm.utm_term || "", landing_url: (typeof window !== "undefined" && window.location) ? window.location.href : "", website: "", // honeypot — always empty on real submits }; const endpoint = "https://f1rststeps.com/amo.php"; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 10000); try { const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: controller.signal, }); clearTimeout(timer); if (res.status === 200) { // Meta Pixel — fire the Lead event once on successful submit. // The base pixel is already initialised in the site head, so we // only call the event here and guard for environments where the // pixel hasn't loaded (e.g. blocked by an ad-blocker). if (typeof fbq !== "undefined") { try { fbq("track", "Lead"); } catch (e) {} } if (formType === "collab") { // Collaboration leads get an inline confirmation rather than the // full thank-you page, matching the previous modal behaviour. setCollabSubmitted(true); return; } const ref = "FS-" + Math.random().toString(36).slice(2, 8).toUpperCase(); navigate({ name: "thanks", id: program.id, ref }); return; } setSubmitError(true); } catch (err) { clearTimeout(timer); setSubmitError(true); } finally { setSubmitting(false); } }; // Inline thank-you when a collaboration enquiry submits successfully. // (For player applications we still navigate to the full /thanks page.) if (formType === "collab" && collabSubmitted && ct) { return (

{ct.thanksTitle}

{ct.thanksBody}

); } // Pick the right header copy + cancel destination based on form type. const isCollab = formType === "collab"; const headTitle = isCollab && ct ? ct.title : t.title; const headHeadline = isCollab && ct ? ct.title : (track ? track.title[lang] : (program ? program.title[lang] : "")); const headSub = isCollab && ct ? ct.sub : (track ? (track.ageBand[lang] + " · " + (typeof track.club === "string" ? track.club : track.club[lang])) : t.sub); const cancelTarget = isCollab ? { name: "collaboration" } : (track ? { name: "program", id: program.id, track: track.id } : { name: "program", id: program.id }); const cancelLabel = isCollab && ct ? ct.cancel : t.cancel; const legalText = isCollab && ct ? ct.legal : t.legal; const submitLabel = isCollab && ct ? ct.submit : t.submit; const firstLabel = isCollab && ct ? ct.first : t.first; const lastLabel = isCollab && ct ? ct.last : t.last; const phoneLabel = isCollab && ct ? ct.phone : t.phone; const emailLabel = isCollab && ct ? ct.email : t.email; return (
{isCollab ? (window.I18N[lang].collabPage && window.I18N[lang].collabPage.eyebrow) || headTitle : headTitle}

{headHeadline}

{headSub}
{/* Honeypot — hidden from real users. Bots that auto-fill every field will trip this. */}
{!isCollab && ( )}
{legalText}
{submitError && (
{lang === "ru" ? "Не удалось отправить заявку. Напишите нам в WhatsApp" : (lang === "es" ? "No se pudo enviar la solicitud. Escríbenos por WhatsApp" : "We couldn't submit your application. Message us on WhatsApp")}
WhatsApp
)}
); } // ---------- THANK YOU ---------- function ThankYouPage({ lang, refCode, navigate }) { const t = window.I18N[lang].thanks; useEffect(() => { window.scrollTo(0, 0); }, []); return (

{t.title}

{t.body}

{refCode &&
{t.ref}: {refCode}
}
); } Object.assign(window, { HomePage, ProgramDetail, ApplyPage, ThankYouPage });