// Mr. Hair Club — Mobile React App // Mobile-first. Designed for 360-430px viewport widths. // Renders inside a phone shell on desktop; full-screen on real phones. const { useState, useEffect, useRef, useMemo } = React; const C = window.COPY; const TEAM = window.TEAM; const BOOKING_URL = window.BOOKING_URL; const WHATSAPP_URL = window.WHATSAPP_URL; const MAPS_URL = window.MAPS_URL; const IG_URL = window.IG_URL; const USD_RATE = window.USD_RATE; const fmtNum = window.fmtNum; const REVIEWS = window.REVIEWS; // ───────────────────────────────────────────────────────────── // PRIMITIVES function BrandLogo({ size = 1 }) { return (
MR. HAIR club
); } function BookLink({ children, secondary, small, block, className, onClick }) { const cls = ["btn", secondary ? "btn-ghost" : "btn-primary", small ? "btn-sm" : "", block ? "btn-block" : "", className || ""].filter(Boolean).join(" "); return ( {children} ); } function SectionHead({ kicker, title, sub }) { return (
{kicker}

{title}

{sub &&

{sub}

}
); } // ───────────────────────────────────────────────────────────── // HEADER + DRAWER function Header({ scrolled, onOpenMenu, lang, setLang }) { return (
); } function LangPill({ lang, setLang }) { return (
{["pt", "en", "es"].map((l) => ( ))}
); } function Drawer({ open, onClose, t, scrollTo }) { if (!open) return null; const links = [ ["services", t.nav.services], ["team", t.nav.team], ["work", t.nav.work], ["club", t.nav.club], ["visit", t.nav.visit], ]; return (
{links.map(([id, label]) => ( { e.preventDefault(); onClose(); scrollTo(id); }} > {label} ))}
{t.nav.book} WhatsApp
Djalma Ulrich, 163 · Copacabana
); } // ───────────────────────────────────────────────────────────── // HERO — text-forward, photo strip below function Hero({ t, lang }) { return (
Copacabana · RJ · desde 2011

{t.hero.title1} {t.hero.title2} {t.hero.title3}

{t.hero.sub}

4.9 · {fmtNum(1512, lang)} {t.authority.reviews} · Google
{t.hero.primary} {t.hero.secondary}
); } function HeroPhotoStrip({ lang }) { const photos = [ { src: "assets/work-real-01.webp", caption: lang === "pt" ? "Allan, cliente desde 2022" : lang === "es" ? "Allan, cliente desde 2022" : "Allan, regular since 2022", }, { src: "assets/work-real-02.webp", caption: lang === "pt" ? "Equipe · joia da casa" : lang === "es" ? "Equipo · orgullo de la casa" : "The team, in their element", }, { src: "assets/storefront.jpg", caption: lang === "pt" ? "Rua Djalma Ulrich, 163" : lang === "es" ? "Calle Djalma Ulrich, 163" : "Djalma Ulrich, 163", }, ]; return (
{photos.map((p, i) => (
{p.caption}
{p.caption}
))}
); } function HeroStats({ t, lang }) { const totalCuts = TEAM.reduce((s, b) => s + b.cuts, 0); const items = [ { big: "4.9★", small: "Google" }, { big: fmtNum(1512, lang), small: t.authority.reviews }, { big: fmtNum(Math.round(totalCuts / 1000)) + "k+", small: t.authority.cuts.split(" ")[0] }, { big: "15", small: t.authority.years.split(" ")[0] }, ]; return (
{items.map((it, i) => (
{it.big}
{it.small}
))}
); } // ───────────────────────────────────────────────────────────── // STATUS CARD — replaces redundant quick actions; shows live open/closed function computeStatus(lang) { // Hours: Mon-Sat 9-20, Sun closed (Rio time approx) const now = new Date(); const day = now.getDay(); // 0 Sun, 6 Sat const mins = now.getHours() * 60 + now.getMinutes(); const openMin = 9 * 60; const closeMin = 20 * 60; const isSun = day === 0; const isOpen = !isSun && mins >= openMin && mins < closeMin; const closesSoon = isOpen && mins >= closeMin - 60; const T = { pt: { open: "Aberto agora", closed: "Fechado", closesAt: "fecha às 20h", closesSoon: "fecha em breve", reopens: "abre amanhã às 9h", reopensMon: "abre segunda às 9h", hint: "Walk-ins aceitos quando há cadeira livre", }, en: { open: "Open now", closed: "Closed", closesAt: "closes at 8pm", closesSoon: "closing soon", reopens: "opens tomorrow at 9am", reopensMon: "opens Monday at 9am", hint: "Walk-ins welcome when chairs are free", }, es: { open: "Abierto ahora", closed: "Cerrado", closesAt: "cierra a las 20h", closesSoon: "cierra pronto", reopens: "abre mañana a las 9h", reopensMon: "abre el lunes a las 9h", hint: "Sin cita según disponibilidad", }, }[lang]; let subtitle = ""; if (isOpen) subtitle = closesSoon ? T.closesSoon : T.closesAt; else if (isSun) subtitle = T.reopensMon; else subtitle = mins < openMin ? (lang === "pt" ? "abre às 9h" : lang === "es" ? "abre a las 9h" : "opens at 9am") : T.reopens; return { isOpen, label: isOpen ? T.open : T.closed, subtitle, hint: T.hint }; } function StatusCard({ lang }) { const [s, setS] = useState(() => computeStatus(lang)); useEffect(() => { setS(computeStatus(lang)); const id = setInterval(() => setS(computeStatus(lang)), 60000); return () => clearInterval(id); }, [lang]); const callTxt = lang === "pt" ? "Ligar" : lang === "es" ? "Llamar" : "Call"; return (
); } // ───────────────────────────────────────────────────────────── // WHY function Why({ t }) { return (
{t.why.items.map((it, i) => (
{it.n}

{it.t}

{it.d}

))}
); } // ───────────────────────────────────────────────────────────── // SERVICES — collapsible accordion with sticky group nav function Services({ t, lang, showUsd = true }) { const groups = t.services.groups; // Track which groups are open. First open by default. const [open, setOpen] = useState(() => groups.map((_, i) => i === 0)); const refs = useRef([]); const toggle = (i) => { setOpen((prev) => prev.map((v, j) => (j === i ? !v : v))); }; const scrollToGroup = (i) => { setOpen((prev) => prev.map((v, j) => (j === i ? true : v))); requestAnimationFrame(() => { const el = refs.current[i]; if (el) { const scroller = document.getElementById("phone-scroll"); if (scroller) { scroller.scrollTo({ top: el.offsetTop - 110, behavior: "smooth" }); } } }); }; return (
{groups.map((g, i) => ( ))}
{groups.map((g, gi) => (
(refs.current[gi] = el)}> {open[gi] && ( )}
))}
); } // ───────────────────────────────────────────────────────────── // TEAM — horizontal carousel function Team({ t, lang }) { const totalCuts = TEAM.reduce((s, b) => s + b.cuts, 0); const scrollRef = useRef(null); const [idx, setIdx] = useState(0); const onScroll = () => { const el = scrollRef.current; if (!el) return; const cardWidth = el.firstChild?.offsetWidth || 1; const gap = 14; const i = Math.round(el.scrollLeft / (cardWidth + gap)); setIdx(Math.min(i, TEAM.length - 1)); }; return (
{fmtNum(totalCuts, lang)}+
{lang === "pt" && "cortes feitos. Seis barbeiros. Todos com nota 5,00."} {lang === "en" && "cuts performed. Six barbers. All rated 5.00."} {lang === "es" && "cortes realizados. Seis barberos. Todos con 5,00."}
{TEAM.map((b) => ( ))}
{TEAM.map((_, i) => ( ))}
); } function BarberCard({ barber, t, lang }) { const hasPhoto = barber.id !== "sebastian"; return (
{hasPhoto ? ( <>
{barber.name} ) : (
)}
{barber.rating.toFixed(2)}

{barber.name}

{t.team.role}
{fmtNum(barber.cuts, lang)} {t.team.cuts}

{t.team.bios[barber.name]}

{barber.tags.map((tag) => ( {t.team.tags[tag]} ))}
{t.team.bookWith} {barber.name}
); } // ───────────────────────────────────────────────────────────── // WORK function Work({ t }) { const tags = ["all", "cuts", "beards", "interior", "before"]; const [filter, setFilter] = useState("all"); // Mobile bento: 2 cols, first cell spans 2 cols (hero), then alternating spans const spans = [ "span-2-col", // big hero "", "span-2-row", // tall "", "", "span-2-col", // wide "", "", ]; return (
{tags.map((tag) => ( ))}
{t.work.slots.slice(0, 8).map((s, i) => { const hide = filter !== "all" && s.tag !== filter; const realSrc = window.WORK_PHOTOS && window.WORK_PHOTOS[s.id]; return (
{t.work.tags[s.tag]}
); })}
{t.work.cta}
); } // ───────────────────────────────────────────────────────────── // CLUB function Club({ t, lang }) { const [tab, setTab] = useState("flex"); const plans = tab === "flex" ? t.club.flex : t.club.essencial; return (
{plans.map((p, i) => (
{p.hot &&
{t.club.most}
}
{p.name}
R$ {p.price} {t.club.monthly}
{t.club.days}
{p.days}
{t.club.includes}
    {p.inc.map((it, j) => (
  • {it}
  • ))}
{p.save &&
{p.save}
} {t.club.subscribe}
))}

{t.club.compare}

{t.club.note}

); } // ───────────────────────────────────────────────────────────── // REVIEWS function Reviews({ t, lang }) { const items = (REVIEWS[lang] || []).filter((r) => !r.placeholder); // Limit to 6 cards on mobile const limit = items.slice(0, 6); return (
{limit.map((r, i) => ( ))}
4.9
{fmtNum(1512, lang)} {t.authority.reviews} · Google
{t.reviews.cta}
); } function ReviewCard({ r, lang }) { const [expanded, setExpanded] = useState(false); const moreLabel = lang === "pt" ? "ler mais" : lang === "es" ? "leer más" : "read more"; const lessLabel = lang === "pt" ? "recolher" : lang === "es" ? "contraer" : "less"; const long = r.text.length > 180; const beforeLabel = lang === "en" ? "Before" : lang === "es" ? "Antes" : "Antes"; const afterLabel = lang === "en" ? "After" : lang === "es" ? "Después" : "Depois"; let photoEl = null; if (r.combinedBeforeAfter && r.photo) { photoEl = (
{beforeLabel} {afterLabel}
); } else if (r.photos && r.photos.length > 0) { photoEl = (
{r.beforeAfter && {beforeLabel}}
); } else if (r.photo) { photoEl = (
); } return (
{photoEl}
★★★★★

"{r.text}"

{long && ( )}
{r.name} · {r.date}
); } // ───────────────────────────────────────────────────────────── // VISIT function Visit({ t, lang }) { const openLabel = lang === "pt" ? "Abrir no Google Maps" : lang === "es" ? "Abrir en Google Maps" : "Open in Google Maps"; return (
); } // ───────────────────────────────────────────────────────────── // FINAL CTA + FOOTER function FinalCta({ t }) { return (

{t.finalCta.title}

{t.finalCta.sub}

{t.finalCta.book}
); } function Footer({ t, lang, setLang }) { return ( ); } // ───────────────────────────────────────────────────────────── // BOTTOM TAB BAR (sticky) function TabBar({ t, lang }) { return ( ); } // ───────────────────────────────────────────────────────────── // APP function App() { const [lang, setLang] = useState("pt"); const [scrolled, setScrolled] = useState(false); const [progress, setProgress] = useState(0); const [drawerOpen, setDrawerOpen] = useState(false); const scrollerRef = useRef(null); const t = C[lang]; useEffect(() => { const el = scrollerRef.current; if (!el) return; const onScroll = () => { setScrolled(el.scrollTop > 32); const max = el.scrollHeight - el.clientHeight; setProgress(max > 0 ? Math.min(1, el.scrollTop / max) : 0); }; onScroll(); el.addEventListener("scroll", onScroll, { passive: true }); return () => el.removeEventListener("scroll", onScroll); }, []); // Lock body scroll when drawer is open (real mobile) useEffect(() => { const el = scrollerRef.current; if (!el) return; if (drawerOpen) el.style.overflow = "hidden"; else el.style.overflow = ""; }, [drawerOpen]); const scrollTo = (id) => { const el = document.getElementById(id); const scroller = scrollerRef.current; if (el && scroller) { scroller.scrollTo({ top: el.offsetTop - 50, behavior: "smooth" }); } }; return ( <>
setDrawerOpen(true)} lang={lang} setLang={setLang} /> setDrawerOpen(false)} t={t} scrollTo={scrollTo} /> ); } ReactDOM.createRoot(document.getElementById("phone-mount")).render();