/* =============================================================
* 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); }
};