/* 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 &&
}
{p.title[lang]}
{p.short[lang]}
);
};
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) => (
))}
{t.how.eyebrow}
{t.how.title}
{t.how.steps.map((s, i) => (
))}
{t.about.eyebrow}
{t.about.title}
{t.about.body}
{t.about.pillars.map((p, i) => (
))}
{t.collab.eyebrow}
{t.collab.title}
{t.collab.body}
{t.history.eyebrow}
{t.history.title}
{t.history.items.map((h, i) => (
))}
);
}
// ---------- 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}
);
}
// ---------- 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 });