/* =============================================================
* Shared components: Navbar, Footer, Buttons, Placeholders, Hooks
* ============================================================= */
const { useState, useEffect, useRef, useCallback, useMemo } = React;
/* ---------- Routing (hash based, lightweight) ---------- */
function useHashRoute() {
const [route, setRoute] = useState(() => window.location.hash.replace(/^#\/?/, "") || "home");
useEffect(() => {
const onHash = () => {
const r = window.location.hash.replace(/^#\/?/, "") || "home";
setRoute(r);
window.scrollTo({ top: 0, behavior: "instant" in window ? "instant" : "auto" });
};
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
return [route, (r) => {window.location.hash = "/" + r;}];
}
function Link({ to, children, className = "", onClick, ...rest }) {
const handleClick = (e) => {
e.preventDefault();
if (onClick) onClick(e);
window.location.hash = "/" + to;
};
return (
{children}
);
}
/* ---------- IntersectionObserver hook for in-view animations ---------- */
function useInView(options = { threshold: 0.25, rootMargin: "0px 0px -10% 0px" }) {
const ref = useRef(null);
const [inView, setInView] = useState(false);
useEffect(() => {
if (!ref.current) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {setInView(true);io.disconnect();}
}, options);
io.observe(ref.current);
return () => io.disconnect();
}, []);
return [ref, inView];
}
/* ---------- Animated counter ---------- */
function CountUp({ to = 0, duration = 1400, suffix = "", className = "" }) {
const [ref, inView] = useInView();
const [val, setVal] = useState(0);
useEffect(() => {
if (!inView) return;
const start = performance.now();
let raf;
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(Math.round(eased * to));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [inView, to, duration]);
return {val.toLocaleString()}{suffix} ;
}
/* ---------- Logo ---------- */
function Logo({ className = "h-10 w-auto" }) {
const src = typeof window !== "undefined" && window.__resources && window.__resources.logo || "assets/logo.png";
return ;
}
/* ---------- Buttons ---------- */
function Button({ as = "button", variant = "navy", size = "md", className = "", children, to, ...rest }) {
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-[15px]",
lg: "px-8 py-4 text-base"
};
const variants = {
gold: "btn-gold",
sky: "btn-sky",
navy: "btn-navy",
ghost: "bg-white/10 text-white border border-white/30 hover:bg-white/20",
outline: "bg-white text-navy border border-navy/15 hover:border-navy/40"
};
const cls = `inline-flex items-center justify-center gap-2 font-display font-bold rounded-full transition-all uppercase tracking-wide ${sizes[size]} ${variants[variant]} ${className}`;
if (to) return {children};
const Tag = as;
return {children} ;
}
/* ---------- Placeholder image (striped, with caption) ---------- */
function Placeholder({ label = "image", className = "", tone = "navy", ratio = "16/9" }) {
const tones = {
navy: "ph-stripes",
warm: "ph-stripes-warm",
sky: "ph-stripes-sky",
dark: "ph-stripes-dark text-white/80"
};
return (
);
}
/* ---------- Top utility bar ---------- */
function UtilityBar() {
const [lang, setLang] = useLang();
// Apply selected language to Google Translate (with retry until the widget is ready)
const applyGoogleLang = (target) => {
let tries = 0;
const attempt = () => {
const ok = typeof window.faSetGoogleLang === "function" && window.faSetGoogleLang(target);
if (!ok && tries++ < 30) setTimeout(attempt, 250);
};
attempt();
};
const choose = (next) => {
setLang(next); // updates locally-translated strings (navbar etc.)
applyGoogleLang(next); // triggers Google Translate for the rest of the page
};
// On first mount, restore the saved language in the Google Translate widget too
useEffect(() => {
applyGoogleLang(lang);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
{/* Contact info removed */}
{t('restore.cta', lang)}
choose('en')} className={`px-1.5 py-0.5 rounded ${lang === 'en' ? 'bg-gold text-navy' : 'hover:bg-white/10'}`}>EN
|
choose('lg')} className={`px-1.5 py-0.5 rounded ${lang === 'lg' ? 'bg-gold text-navy' : 'hover:bg-white/10'}`}>LG
|
choose('sw')} className={`px-1.5 py-0.5 rounded ${lang === 'sw' ? 'bg-gold text-navy' : 'hover:bg-white/10'}`}>SW
);
}
/* ---------- Navbar ---------- */
function Navbar({ route }) {
const [lang] = useLang();
const [open, setOpen] = useState(false);
const [progOpen, setProgOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const navItems = [
{ label: t('nav.about', lang), to: "about" },
{ label: "Be A Man", to: "be-a-man" },
{ label: "Nurturing Bonds", to: "nurturing-bonds" },
{ label: "Mobile Kanaabe", to: "mobile-kanaabe" },
{ label: "Champion Virtues", to: "champion-virtues" },
{ label: t('nav.stories', lang), to: "stories" },
{ label: t('nav.getInvolved', lang), to: "get-involved" },
{ label: t('nav.contact', lang), to: "contact" }];
const programsMenu = [
{ title: t('programs.beAMan', lang), desc: t('programs.beAMan.desc', lang), to: "be-a-man" },
{ title: t('programs.nurturingBonds', lang), desc: t('programs.nurturingBonds.desc', lang), to: "nurturing-bonds" },
{ title: t('programs.mobileKanaabe', lang), desc: t('programs.mobileKanaabe.desc', lang), to: "mobile-kanaabe" }
];
const isActive = (to) => route === to || to === "programs" && ["be-a-man", "nurturing-bonds", "mobile-kanaabe"].includes(route);
return (
<>
>);
}
/* ---------- Footer ---------- */
function Footer() {
return (
Restoring fatherhood. Building nations.
An initiative of The Remnant Generation. We build the men the world is waiting for — from boyhood to fatherhood to legacy.
{[
{ k: "facebook", href: "#" },
{ k: "instagram", href: "https://www.instagram.com/fathersariseofficial/?__pwa=1#" },
{ k: "youtube", href: "https://www.youtube.com/@FathersArse" },
{ k: "x", href: "#" },
{ k: "whatsapp", href: "https://wa.me/256700669586" }
].map((s) =>
)}
About
Our Story
Leadership
Reports
Privacy
Programs
Be a Man
Nurturing Bonds
Mobile Kanaabe
The Forge
Get Involved
Donate
Volunteer
Partner
Churches
Stay In Touch
Monthly field notes from the Forge. No spam.
© 2026 Fathers Arise. A registered Ugandan NGO. All rights reserved.
);
}
function SocialIcon({ name }) {
const props = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "currentColor" };
switch (name) {
case "facebook":return ;
case "instagram":return ;
case "youtube":return ;
case "x":return ;
case "whatsapp":return ;
default:return null;
}
}
/* ---------- Floating WhatsApp ---------- */
function WhatsAppFloat() {
return (
);
}
/* ---------- Section title ---------- */
function SectionTitle({ eyebrow, title, lede, light = false, center = false, className = "" }) {
return (
{eyebrow &&
{eyebrow}
}
{title}
{lede &&
{lede}
}
);
}
/* ---------- Page hero (sub-page header) ---------- */
function PageHero({ eyebrow, title, lede, breadcrumb, bgImage }) {
return (
{bgImage ? (
<>
>
) : (
<>
>
)}
{breadcrumb &&
{breadcrumb.map((b, i) =>
{i > 0 && / }
{b.to ? {b.label} : {b.label} }
)}
}
{eyebrow &&
{eyebrow}
}
{title}
{lede &&
{lede}
}
);
}
/* ---------- Field components for forms ---------- */
function Field({ label, children, hint, error, id }) {
return (
{label}
{children}
{hint && {hint} }
{error && {error} }
);
}
function Input(props) {
return (
);
}
function Textarea(props) {
return (
);
}
function Select(props) {
return (
);
}
/* Expose components globally for other Babel files */
Object.assign(window, {
useHashRoute, Link, useInView, CountUp, Logo, Button, Placeholder,
Navbar, Footer, WhatsAppFloat, SectionTitle, PageHero,
Field, Input, Textarea, Select, SocialIcon
});