/* ============================================================= * CMS service layer (mock) * In production this swaps to Contentful/Sanity/Strapi via env vars. * Each method returns a Promise so the swap is mechanical. * ============================================================= */ const _delay = (ms) => new Promise((r) => setTimeout(r, ms)); const CMS_FIXTURES = { impact: { fathersReached: 547, activeForgeGroups: 82, communitiesTransformed: 25, countriesEngaged: 4, }, forgeGoal: { current: 82, target: 1000 }, forgeGroups: [ // x/y are SVG percentages on the Uganda placeholder map { id: "kla-01", name: "Forge Kampala Central", region: "Kampala", members: 24, x: 53, y: 60, status: "active" }, { id: "wak-02", name: "Forge Wakiso", region: "Wakiso", members: 18, x: 50, y: 58, status: "active" }, { id: "jin-03", name: "Forge Jinja Riverside", region: "Jinja", members: 31, x: 64, y: 62, status: "active" }, { id: "mbl-04", name: "Forge Mbale", region: "Mbale", members: 22, x: 78, y: 53, status: "growing" }, { id: "gul-05", name: "Forge Gulu Northern", region: "Gulu", members: 19, x: 50, y: 25, status: "active" }, { id: "lir-06", name: "Forge Lira", region: "Lira", members: 14, x: 60, y: 32, status: "growing" }, { id: "mba-07", name: "Forge Mbarara", region: "Mbarara", members: 27, x: 30, y: 70, status: "active" }, { id: "ksh-08", name: "Forge Kasese", region: "Kasese", members: 12, x: 18, y: 55, status: "growing" }, { id: "fpr-09", name: "Forge Fort Portal", region: "Fort Portal", members: 16, x: 22, y: 47, status: "active" }, { id: "soroti-10", name: "Forge Soroti", region: "Soroti", members: 21, x: 70, y: 42, status: "active" }, { id: "ent-11", name: "Forge Entebbe", region: "Entebbe", members: 17, x: 50, y: 67, status: "active" }, { id: "ara-12", name: "Forge Arua", region: "Arua", members: 9, x: 30, y: 18, status: "growing" }, ], events: [ { id: "ev-01", title: "National Fathers Conference 2026", date: "2026-06-14", location: "Kampala Serena Conference Centre", type: "Conference", summary: "Three days of teaching, worship and forging brotherhood. Open to fathers and father-figures across East Africa.", cta: "Register", image: "uploads/Forge 1-104.jpg" }, { id: "ev-02", title: "Forge Leaders Retreat — Jinja", date: "2026-05-23", location: "Source of the Nile Resort, Jinja", type: "Retreat", summary: "Equipping the next 200 Forge group leaders ahead of the regional rollout.", cta: "Apply", image: "uploads/forge meeting event section.jpg" }, { id: "ev-03", title: "Mobile Kanaabe — Mbale Outreach", date: "2026-05-30", location: "Mbale Town Square", type: "Outreach", summary: "Bringing fatherhood teaching, free clinics and family games to the heart of Mbale.", cta: "Volunteer", image: "uploads/Screenshot (119).png" }, { id: "ev-04", title: "Be a Man — Be a Father Workshop", date: "2026-06-07", location: "Watoto Church, Kampala", type: "Workshop", summary: "A one-day workshop for first-time and expectant fathers, with mentor-fathers on hand.", cta: "Register", image: "uploads/FATHERS ARISE MEETING-66.jpg" }, { id: "ev-05", title: "Nurturing Bonds — Father+Child Day", date: "2026-07-12", location: "Entebbe Botanical Gardens", type: "Family Day", summary: "Picnic, games, and structured bonding activities for fathers and their children aged 4–14.", cta: "Register", image: "uploads/FATHERS ARISE-135.jpg" }, ], stories: [ { id: "st-01", kind: "video", title: "From absent to anchor: Busega's story", excerpt: "After fifteen years of distance, a Forge group walked Joseph back into his family.", author: "Joseph K., Busega", tag: "Testimony", duration: "4:32", videoId: "pAoj2VW0B4A", videoEmbed: '', }, { id: "st-02", kind: "video", title: "Why fatherhood is Uganda's quietest crisis", excerpt: "When 1 in 4 children grow up without a present father, the consequences ripple through every system.", author: "Pastor D. Mukasa", tag: "Insight", duration: "5:20", videoId: "QCXdFzBOw5E", videoEmbed: '', }, { id: "st-03", kind: "video", title: "Inside a Forge group: the Wakiso brothers", excerpt: "A weekly meeting that's reshaping a neighbourhood, one accountability circle at a time.", author: "Forge Wakiso", tag: "Inside The Forge", duration: "6:18", videoId: "k-S-yAhTfRc", videoEmbed: '', }, { id: "st-04", kind: "video", title: "Mobile Kanaabe: the truck that carries fatherhood", excerpt: "How a converted lorry brings teaching, games and grace to villages without a Forge.", author: "Mobile Kanaabe Team", tag: "Field Notes", duration: "3:45", videoId: "YQQ-gf7cSqY", videoEmbed: '', }, { id: "st-05", kind: "video", title: "A daughter's letter to her dad", excerpt: "What changed in the Nakimera home when a Forge group started meeting on Tuesdays.", author: "Esther N., Nansana", tag: "Testimony", duration: "3:05", videoId: "k_tlZ6YIHug", videoEmbed: '', }, { id: "st-06", kind: "video", title: "Discipleship that fits a working father's week", excerpt: "We rebuilt the curriculum around boda rides, night shifts and Sunday markets.", author: "Curriculum Team", tag: "Curriculum", duration: "8:15", videoId: "KLbq7toxzI8", videoEmbed: '', }, ], testimonials: [ { id: "t-01", quote: "The Forge gave me brothers who would not let me drift. My children have their father back, and my wife has her husband.", author: "Joseph K.", where: "Forge Busega, Kampala", }, { id: "t-02", quote: "I came thinking I was already a good father. I left understanding I had been a good provider — and a stranger.", author: "Patrick O.", where: "Forge Lira", }, { id: "t-03", quote: "Mobile Kanaabe rolled into our trading centre and stayed three days. By the end, twelve men had committed to start a Forge.", author: "Pastor M. Wanyama", where: "Mbale District", }, ], }; // Public CMS API. Reads live from Supabase if available; falls back to fixtures // so the site never breaks if Supabase has a hiccup or hasn't been seeded yet. // // To swap content from the OS / Supabase: edit a row in the corresponding table // (events, stories, testimonials, team_members, forge_groups). The website // re-fetches on next page load — no deploy required. function _hasSupabase() { return typeof window !== "undefined" && window.sb && typeof window.sb.from === "function"; } // Map a Supabase row to the shape the website components expect function _mapEvent(r) { return r && { id: r.id, title: r.title, date: r.date, location: r.location, type: r.type, summary: r.summary, cta: r.cta || "Register", image: r.image_url }; } function _mapStory(r) { return r && { id: r.id, kind: r.kind || "video", title: r.title, excerpt: r.excerpt, author: r.author, tag: r.tag, duration: r.duration, videoId: r.video_id, coverImage: r.cover_image }; } function _mapTestimonial(r) { return r && { id: r.id, quote: r.quote, author: r.author, where: r.location }; } function _mapForgeGroup(r) { return r && { id: r.id, name: r.name, region: r.region, members: r.members, x: r.x, y: r.y, status: r.status }; } function _mapTeam(r) { return r && { id: r.id, name: r.name, role: r.role, img: r.image_url, bio: r.bio, displayOrder: r.display_order }; } async function _fetchTable(table, mapper, orderCol, fixture) { if (!_hasSupabase()) return fixture; try { const { data, error } = await window.sb.from(table).select("*").order(orderCol || "created_at", { ascending: true }); if (error) { console.warn("cms." + table + ":", error.message); return fixture; } if (!data || data.length === 0) return fixture; return data.map(mapper); } catch (e) { console.warn("cms." + table + " failed:", e.message); return fixture; } } // ─── Phase 3.6 — Site content (text edits from OS) ──────────────────────────── // Cache so pages don't refetch on every render. const _siteContentCache = {}; async function _fetchSiteContent(pageKey) { if (_siteContentCache[pageKey]) return _siteContentCache[pageKey]; const sb = (typeof window !== "undefined") && window.sb; if (!sb || !sb.from) return {}; try { const { data, error } = await sb .from("site_content") .select("key, section, value") .eq("page", pageKey); if (error) { return {}; } const grouped = {}; (data || []).forEach(row => { const sec = row.section || "_"; // Extract field from dotted key: "home.hero.headline" → "headline" const parts = (row.key || "").split("."); const fieldKey = parts.length >= 3 ? parts.slice(2).join(".") : (parts[parts.length-1] || "_"); if (!grouped[sec]) grouped[sec] = {}; grouped[sec][fieldKey] = row.value; }); _siteContentCache[pageKey] = grouped; return grouped; } catch (e) { return {}; } } // Synchronous helper: returns value if already cached, otherwise fallback. // Use in render: cms.text("home", "hero", "headline", "Default headline") function _text(pageKey, sectionKey, fieldKey, fallback) { const page = _siteContentCache[pageKey]; if (!page) return fallback || ""; const section = page[sectionKey]; if (!section) return fallback || ""; return section[fieldKey] != null && section[fieldKey] !== "" ? section[fieldKey] : (fallback || ""); } window.cms = { // Counters + goal stay as fixtures for now (Phase 3 will compute from real data) // Phase 3.11: live counters — query real counts from Supabase, fall back to fixtures async getImpactCounters() { const sb = (typeof window !== "undefined") && window.sb; if (!sb || !sb.from) { await _delay(120); return CMS_FIXTURES.impact; } try { const [fathers, forgeGroups, beam, fromInq] = await Promise.all([ sb.from("fa_father_inquiries").select("id", { count: "exact", head: true }), sb.from("forge_groups").select("id", { count: "exact", head: true }), sb.from("fa_beam_students").select("id", { count: "exact", head: true }), sb.from("fa_father_inquiries").select("location", { count: "exact" }).limit(1000) ]); const fathersCount = (fathers.count != null ? fathers.count : 0) + (beam.count != null ? beam.count : 0); const groupsCount = forgeGroups.count != null ? forgeGroups.count : 0; const locations = new Set(); (fromInq.data || []).forEach(r => { if (r && r.location) locations.add(String(r.location).split(/[,\s]+/)[0]); }); const communities = locations.size || CMS_FIXTURES.impact.communitiesTransformed; return { fathersReached: fathersCount > 0 ? fathersCount : CMS_FIXTURES.impact.fathersReached, activeForgeGroups: groupsCount > 0 ? groupsCount : CMS_FIXTURES.impact.activeForgeGroups, communitiesTransformed: communities, countriesEngaged: CMS_FIXTURES.impact.countriesEngaged // manual until you track this }; } catch (e) { console.warn("getImpactCounters live fetch failed, using fixtures:", e && e.message); return CMS_FIXTURES.impact; } }, // Phase 3.11: live forge progress async getForgeGoal() { const sb = (typeof window !== "undefined") && window.sb; if (!sb || !sb.from) { await _delay(120); return CMS_FIXTURES.forgeGoal; } try { const { count } = await sb.from("forge_groups").select("id", { count: "exact", head: true }); return { current: count != null ? count : CMS_FIXTURES.forgeGoal.current, target: CMS_FIXTURES.forgeGoal.target }; } catch (e) { return CMS_FIXTURES.forgeGoal; } }, // Live from Supabase (with fixture fallback) async getForgeGroups() { return _fetchTable("forge_groups", _mapForgeGroup, "created_at", CMS_FIXTURES.forgeGroups); }, async getEvents() { return _fetchTable("events", _mapEvent, "date", CMS_FIXTURES.events); }, async getStories() { return _fetchTable("stories", _mapStory, "created_at", CMS_FIXTURES.stories); }, async getTestimonials() { return _fetchTable("testimonials", _mapTestimonial, "created_at", CMS_FIXTURES.testimonials); }, async getTeam() { return _fetchTable("team_members", _mapTeam, "display_order", null); }, // Phase 3.6: Site content — text edits Hudson/Isaac make in the OS async getSiteContent(pageKey) { return _fetchSiteContent(pageKey); }, text(pageKey, sectionKey, fieldKey, fallback) { return _text(pageKey, sectionKey, fieldKey, fallback); } };