/* global React */ // FirstSteps — standalone pages: About, Partner Clubs, Collaboration (+ Collaboration form modal) const { useState: useStateP, useEffect: useEffectP, useMemo: useMemoP, useRef: useRefP } = React; // ---------- ABOUT US ---------- function AboutPage({ lang, navigate }) { const t = window.I18N[lang].aboutPage; useEffectP(() => { window.scrollTo(0, 0); }, []); return (
{t.eyebrow}
FirstSteps mark
{t.kicker}

{t.lead}

{t.founderTag} Olzhas Akmolda
{t.story.map((chapter, i) => (
0{i + 1}

{chapter.h}

{chapter.p.map((para, j) =>

{para}

)}
))}
); } // ---------- PARTNER CLUBS ---------- function PartnersPage({ lang, navigate }) { const t = window.I18N[lang].partnersPage; const labels = (window.PARTNER_DETAIL_LABELS && window.PARTNER_DETAIL_LABELS[lang]) || (window.PARTNER_DETAIL_LABELS && window.PARTNER_DETAIL_LABELS.en) || {}; useEffectP(() => { window.scrollTo(0, 0); }, []); return (
{t.eyebrow}

{t.title}

{t.lead}

{window.PARTNERS.map((club) => { const c = (t.clubs && t.clubs[club.name]) || { tagline: club.loc, text: "" }; const hasDetails = !!(window.PARTNER_DETAILS && window.PARTNER_DETAILS[club.name]); const onOpen = () => { if (hasDetails) navigate({ name: "partners", id: club.id }); }; return (
{ if (hasDetails && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onOpen(); } }} aria-label={hasDetails ? `${club.name} — ${labels.open || "Details"}` : undefined} >
{club.name}

{club.name}

{c.tagline}

{c.text}

{hasDetails && ( )}
); })}

{t.ctaTitle}

{t.ctaBody}

); } // ---------- CLUB DETAIL PAGE (full page, own URL) ---------- // Renders at #/partners/{clubId}. Pulls language-neutral facts plus history // and per-player role notes from window.PARTNER_DETAILS, indexed by club // display name (window.PARTNERS provides the id → name mapping). function ClubDetailPage({ lang, clubId, navigate }) { const labels = (window.PARTNER_DETAIL_LABELS && window.PARTNER_DETAIL_LABELS[lang]) || (window.PARTNER_DETAIL_LABELS && window.PARTNER_DETAIL_LABELS.en) || {}; const club = (window.PARTNERS || []).find((p) => p.id === clubId); const details = club && window.PARTNER_DETAILS && window.PARTNER_DETAILS[club.name]; useEffectP(() => { window.scrollTo(0, 0); }, [clubId]); // Unknown slug — send the user back to the partner-clubs index. if (!club || !details) { return (

Club not found.

); } const localised = (val) => (val && typeof val === "object" && !Array.isArray(val)) ? (val[lang] || val.en || "") : (val || ""); return (
{club.name}
{localised(details.league)}

{club.name}

{labels.founded}
{details.founded}
{labels.city}
{details.city}
{labels.stadium}
{details.stadium}

{labels.history}

{localised(details.history)}

{labels.trophies}

    {(details.trophies || []).map((tr, i) => (
  • {tr.name} {tr.year}
  • ))}

{labels.stars}

    {(details.stars || []).map((s, i) => (
  • {s.name} {localised(s.role)}
  • ))}

{(window.I18N[lang].partnersPage && window.I18N[lang].partnersPage.ctaTitle) || labels.cta}

{(window.I18N[lang].partnersPage && window.I18N[lang].partnersPage.ctaBody) || ""}

); } // ---------- COLLABORATION ---------- function CollaborationPage({ lang, navigate }) { const t = window.I18N[lang].collabPage; useEffectP(() => { window.scrollTo(0, 0); }, []); // Both "Apply" buttons on this page route to the single, canonical form // URL (#/apply/form) so every form on the site shares the same address. // The form type ("collab") travels in sessionStorage — see app.jsx. const openCollabForm = () => navigate({ name: "apply", type: "collab", returnTo: "collaboration" }); return (
{t.eyebrow}

{t.title}

{t.lead}

{t.bullets.map((b, i) => (
0{i + 1}

{b.h}

{b.b}

))}

{t.ctaTitle}

{t.ctaBody}

); } // World globe with country silhouettes + connection lines spreading from Spain. // Uses D3.geoOrthographic + topojson world-atlas (loaded from CDN) for the country // outlines so the silhouettes are geographically real, with the centre dot fixed // on Spain. function GlobeBackdrop() { const [countries, setCountries] = useStateP(null); // Load world atlas TopoJSON once. useEffectP(() => { let cancelled = false; (async () => { try { // Wait briefly for d3 + topojson script tags to attach to window. for (let i = 0; i < 20 && (!window.d3 || !window.topojson); i++) { await new Promise((r) => setTimeout(r, 100)); } if (!window.d3 || !window.topojson) return; const res = await fetch("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json"); if (!res.ok) return; const world = await res.json(); const features = window.topojson.feature(world, world.objects.countries).features; if (!cancelled) setCountries(features); } catch (e) { // Silent fail — globe still works without silhouettes (graticule + arcs). } })(); return () => { cancelled = true; }; }, []); // Geometry constants. viewBox is square; sphere fills it almost edge-to-edge. const cx = 360, cy = 360, R = 320; const spain = { lat: 40.4, lon: -3.7 }; // Madrid // Build a d3 orthographic projection centred on Spain — used for both country // outlines (via d3.geoPath) and for our connection-arc endpoints. const projection = useMemoP(() => { if (!window.d3) return null; return window.d3.geoOrthographic() .scale(R) .translate([cx, cy]) .rotate([-spain.lon, -spain.lat]) .clipAngle(90); }, []); const geoPath = useMemoP(() => { if (!projection || !window.d3) return null; return window.d3.geoPath(projection); }, [projection]); // Origin (Spain) projects to the centre by construction. const origin = { x: cx, y: cy }; // Connection targets — same set as before. const targets = [ { lat: 39.9, lon: 116.4 }, // Beijing { lat: 35.7, lon: 139.7 }, // Tokyo { lat: 51.0, lon: 71.4 }, // Astana { lat: 28.6, lon: 77.2 }, // Delhi { lat: -1.3, lon: 36.8 }, // Nairobi { lat: -33.9, lon: 18.4 }, // Cape Town { lat: -34.6, lon: -58.4 }, // Buenos Aires { lat: -23.5, lon: -46.6 }, // São Paulo { lat: 19.4, lon: -99.1 }, // Mexico City { lat: 40.7, lon: -74.0 }, // New York { lat: 37.8, lon: -122.4 }, // San Francisco { lat: -33.9, lon: 151.2 }, // Sydney { lat: 25.3, lon: 55.3 }, // Dubai { lat: 1.35, lon: 103.8 }, // Singapore { lat: 55.7, lon: 37.6 }, // Moscow { lat: 60.2, lon: 24.9 }, // Helsinki { lat: 64.1, lon: -21.9 }, // Reykjavik { lat: 6.5, lon: 3.4 }, // Lagos ]; // Project a target via d3 if available, fall back to manual orthographic. function projectPoint(p) { if (projection) { const r = projection([p.lon, p.lat]); if (r) return { x: r[0], y: r[1], visible: true }; } // Fallback manual orthographic const lat0 = (spain.lat * Math.PI) / 180; const lon0 = (spain.lon * Math.PI) / 180; const lat = (p.lat * Math.PI) / 180; const lon = (p.lon * Math.PI) / 180; const cosc = Math.sin(lat0) * Math.sin(lat) + Math.cos(lat0) * Math.cos(lat) * Math.cos(lon - lon0); const x = Math.cos(lat) * Math.sin(lon - lon0); const y = Math.cos(lat0) * Math.sin(lat) - Math.sin(lat0) * Math.cos(lat) * Math.cos(lon - lon0); return { x: cx + R * x, y: cy - R * y, visible: cosc >= 0 }; } // Build the arc list. Back-side targets are nudged to the rim so we still // get the radiating "connections" feel — geographically loose but visually right. const lines = targets.map((tgt, i) => { const p = projectPoint(tgt); let ex = p.x, ey = p.y; if (!p.visible) { const dx = p.x - cx, dy = p.y - cy; const dist = Math.sqrt(dx * dx + dy * dy) || 1; ex = cx + (dx / dist) * R * 0.96; ey = cy + (dy / dist) * R * 0.96; } // Lift the quadratic Bezier control point outward for a curved feel. const mx = (origin.x + ex) / 2; const my = (origin.y + ey) / 2; const vx = mx - cx, vy = my - cy; const vlen = Math.sqrt(vx * vx + vy * vy) || 1; const lift = 60; const ccx = mx + (vx / vlen) * lift; const ccy = my + (vy / vlen) * lift; return { id: i, ex, ey, ccx, ccy, delay: (i % 8) * 0.4 }; }); // Longitudes for the graticule, sampled as polylines (only visible side). const longitudes = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150]; function graticulePolyline(lonDeg) { const pts = []; for (let latDeg = -90; latDeg <= 90; latDeg += 6) { const r = projectPoint({ lat: latDeg, lon: lonDeg }); if (r.visible) pts.push(r.x.toFixed(1) + "," + r.y.toFixed(1)); } return pts.length >= 2 ? pts.join(" ") : null; } return ( ); } // ---------- COLLABORATION FORM (modal) ---------- function CollaborationFormModal({ lang, onClose }) { const t = window.I18N[lang].collabForm; const [values, setValues] = useStateP({ first: "", last: "", email: "", website: "" }); const [errors, setErrors] = useStateP({}); const [submitting, setSubmitting] = useStateP(false); const [submitError, setSubmitError] = useStateP(false); const [submitted, setSubmitted] = useStateP(false); const phoneRef = useRefP(null); // UTM capture (same approach as the player form). const utm = useMemoP(() => { 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 (e) { keys.forEach((k) => { out[k] = ""; }); } return out; }, []); const set = (k) => (e) => setValues({ ...values, [k]: e.target.value }); const validate = () => { const e = {}; if (!values.first.trim()) e.first = t.errors.first; if (!values.last.trim()) e.last = t.errors.last; if (!phoneRef.current || !phoneRef.current.isValid()) e.phone = t.errors.phone; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) e.email = t.errors.email; return e; }; const submit = async (e) => { e.preventDefault(); if (submitting) return; // Honeypot if (values.website && values.website.trim() !== "") return; const errs = validate(); setErrors(errs); if (Object.keys(errs).length > 0) { const f = document.querySelector(".collab-form .field.error input"); if (f) f.focus(); return; } setSubmitError(false); setSubmitting(true); 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(), // Defaults for the other fields the AmoCRM endpoint expects from the player form, // so AmoCRM stays consistent across both lead sources. age: "", current_club: "", position: "", video_link: "", program: "Collaboration", // fixed value — AmoCRM lead title becomes "First Last — Collaboration" 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: "", }; 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 Lead on successful submit (pixel is already // initialised in the site head, this only triggers the event). if (typeof fbq !== "undefined") { try { fbq("track", "Lead"); } catch (e) {} } setSubmitted(true); } else { setSubmitError(true); } } catch (err) { clearTimeout(timer); setSubmitError(true); } finally { setSubmitting(false); } }; return (
e.stopPropagation()}> {!submitted ? (
{window.I18N[lang].collabPage.eyebrow}

{t.title}

{t.sub}
{/* Honeypot */}
{t.legal}
{submitError && (
{lang === "ru" ? "Не удалось отправить заявку. Напишите нам в WhatsApp" : (lang === "es" ? "No se pudo enviar la solicitud. Escríbenos por WhatsApp" : "We couldn't submit your enquiry. Message us on WhatsApp")}
WhatsApp
)}
) : (

{t.thanksTitle}

{t.thanksBody}

)}
); } Object.assign(window, { AboutPage, PartnersPage, ClubDetailPage, CollaborationPage, CollaborationFormModal, GlobeBackdrop });