/* global React */
const { useState, useEffect, useRef, useMemo } = React;
// ---------- Lucide icon helper ----------
function Icon({ name, size = 18, className = "", strokeWidth = 1.75 }) {
const ref = useRef(null);
useEffect(() => {
if (window.lucide && ref.current) {
ref.current.innerHTML = "";
const el = document.createElement("i");
el.setAttribute("data-lucide", name);
el.setAttribute("width", String(size));
el.setAttribute("height", String(size));
el.setAttribute("stroke-width", String(strokeWidth));
ref.current.appendChild(el);
window.lucide.createIcons({ attrs: { width: size, height: size, "stroke-width": strokeWidth } });
}
}, [name, size, strokeWidth]);
return ;
}
// ---------- Brand mark used in nav + modals ----------
function LogoMark({ size = 28 }) {
return (
);
}
// ---------- Decorative orbital background ----------
function OrbitalBackdrop() {
return (
{/* center orbit ring */}
{/* cyan glow (sun replaced by tech aura) */}
{/* signature star — echoes the logo */}
);
}
// ---------- Pricing rendering ----------
function PriceBlock({ pricing }) {
if (pricing.kind === "quote") {
return (
Valor Sob Consulta
{pricing.label &&
{pricing.label}
}
);
}
if (pricing.kind === "fixed") {
return (
{pricing.value}
{pricing.caption}
);
}
// tiered
return (
{pricing.heading &&
{pricing.heading}
}
{pricing.tiers.map((t, i) =>
{t.label}
{t.value}
)}
{pricing.note &&
{pricing.note}
}
);
}
// ---------- Glossary term tooltip ----------
// Tooltip renders into a portal at document.body so it escapes any
// `overflow: hidden` / `transform` / stacking-context clipping from ancestors.
function Term({ term, display }) {
const def = (window.GLOSSARY || {})[term];
const [open, setOpen] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0, flipDown: false });
const triggerRef = useRef(null);
const computePosition = React.useCallback(() => {
if (!triggerRef.current) return;
const r = triggerRef.current.getBoundingClientRect();
const flipDown = r.top < 160;
const x = r.left + r.width / 2;
const y = flipDown ? r.bottom + 8 : r.top - 8;
setPos({ x, y, flipDown });
}, []);
// While open: track scroll/resize/wheel so the tooltip follows the trigger.
useEffect(() => {
if (!open) return;
computePosition();
const onScroll = () => computePosition();
window.addEventListener('scroll', onScroll, true);
window.addEventListener('resize', onScroll);
return () => {
window.removeEventListener('scroll', onScroll, true);
window.removeEventListener('resize', onScroll);
};
}, [open, computePosition]);
// Close on outside click and ESC
useEffect(() => {
if (!open) return;
const onDoc = (e) => {
if (triggerRef.current && !triggerRef.current.contains(e.target)) setOpen(false);
};
const onKey = (e) => {if (e.key === "Escape") setOpen(false);};
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [open]);
if (!def) {
return {display || term};
}
const tooltip = open && ReactDOM.createPortal(
,
document.body
);
return (
setOpen(true)}
onMouseLeave={() => setOpen(false)}>
{e.stopPropagation();setOpen((o) => !o);}}
onKeyDown={(e) => {if (e.key === 'Enter' || e.key === ' ') {e.preventDefault();setOpen((o) => !o);}}}
className="cursor-help inline-flex items-baseline gap-[3px] border-b border-dotted border-cy-400/55 hover:border-cy-300 transition-colors">
{display || term}
?
{tooltip}
);
}
// Walk a string and wrap every glossary term in a element.
// Returns either the original string (no matches) or an array of strings+elements.
function renderWithTerms(text) {
if (typeof text !== 'string' || !window.GLOSSARY) return text;
const keys = Object.keys(window.GLOSSARY).sort((a, b) => b.length - a.length);
if (keys.length === 0) return text;
const escaped = keys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const re = new RegExp('(' + escaped.join('|') + ')', 'gi');
const wordChar = /[\w\u00C0-\u017F]/;
const parts = [];
let last = 0;
let m;
while ((m = re.exec(text)) !== null) {
const before = text[m.index - 1];
const after = text[m.index + m[0].length];
// Skip if embedded inside a longer word (e.g. 'API' inside 'APIzer')
if (before && wordChar.test(before) || after && wordChar.test(after)) continue;
if (m.index > last) parts.push(text.slice(last, m.index));
const canonical = keys.find((k) => k.toLowerCase() === m[0].toLowerCase()) || m[0];
parts.push();
last = m.index + m[0].length;
}
if (parts.length === 0) return text;
if (last < text.length) parts.push(text.slice(last));
return parts;
}
// ---------- Service card ----------
function ServiceCard({ svc, onQuote, variant = "default" }) {
const accent = {
software: { ring: "rgba(26,182,240,.40)", chip: "bg-cy-400/10 text-cy-200 border-cy-400/30", dot: "#1AB6F0", icon: "Code2" },
redes: { ring: "rgba(26,182,240,.35)", chip: "bg-cy-400/10 text-cy-100 border-cy-400/25", dot: "#3FC4FA", icon: "Network" },
telecom: { ring: "rgba(124,218,254,.45)", chip: "bg-cy-200/10 text-cy-100 border-cy-200/25", dot: "#7CDAFE", icon: "RadioTower" },
smart: { ring: "rgba(217,225,236,.30)", chip: "bg-white/5 text-ice-200 border-white/15", dot: "#D9E1EC", icon: "Home" },
suporte: { ring: "rgba(217,225,236,.22)", chip: "bg-white/5 text-ice-200 border-white/10", dot: "#AEB9C8", icon: "Wrench" }
}[svc.category];
return (
{/* corner accent line */}
{/* header row */}
{/* title + description */}
{svc.title}
{renderWithTerms(svc.short)}
{/* deliverables */}
{svc.deliverables.map((d, i) =>
-
{renderWithTerms(d)}
)}
{/* tags */}
{svc.tags.map((t, i) =>
{renderWithTerms(t)}
)}
{/* footer: price stacked above full-width CTA */}
);
}
// ---------- Quote modal ----------
function QuoteModal({ service, onClose }) {
const [form, setForm] = useState({ nome: "", empresa: "", whatsapp: "", demanda: "" });
const [state, setState] = useState("idle"); // idle | sending | success | error
const firstRef = useRef(null);
useEffect(() => {
if (service) setTimeout(() => firstRef.current?.focus(), 80);
const onKey = (e) => {if (e.key === "Escape") onClose();};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [service, onClose]);
if (!service) return null;
const onSubmit = (e) => {
e.preventDefault();
if (!form.nome || !form.whatsapp) return;
setState("sending");
// Build a structured WhatsApp message and open it in a new tab.
const lines = [
`Olá, equipe SUN TECH! Vim pelo site solicitar orçamento:`,
``,
`Serviço: ${service.code} — ${service.title}`,
`Nome: ${form.nome}`];
if (form.empresa) lines.push(`Empresa: ${form.empresa}`);
lines.push(`WhatsApp: ${form.whatsapp}`);
if (form.demanda) {
lines.push(``, `Detalhes:`, form.demanda);
}
const phone = typeof window !== 'undefined' && window.WHATSAPP_PHONE || "5534999999999";
const url = `https://wa.me/${phone}?text=${encodeURIComponent(lines.join("\n"))}`;
window.open(url, "_blank", "noopener,noreferrer");
setState("success");
};
return (
{/* overlay */}
{/* top accent */}
{service.code} · Solicitação de orçamento
{service.title}
{state !== "success" ?
:
WhatsApp aberto em nova aba
Sua mensagem foi pré-preenchida. Basta apertar enviar.
Caso o WhatsApp não tenha aberto, verifique se há bloqueio de pop-ups.
}
);
}
function Field({ label, children }) {
return (
);
}
// ---------- Legal modal: Terms of Service + Privacy Policy ----------
function LegalModal({ doc, onClose, onSwitch }) {
const scrollRef = useRef(null);
useEffect(() => {
if (!doc) return;
// Reset scroll on doc change
if (scrollRef.current) scrollRef.current.scrollTop = 0;
// Lock body scroll
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const onKey = (e) => {if (e.key === "Escape") onClose();};
document.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = prev;
document.removeEventListener("keydown", onKey);
};
}, [doc, onClose]);
if (!doc) return null;
const content = doc === "terms" ? window.LEGAL_TERMS : window.LEGAL_PRIVACY;
const otherDoc = doc === "terms" ? "privacy" : "terms";
const otherLabel = doc === "terms" ? "Ver Política de Privacidade" : "Ver Termos de Serviço";
return (
{/* top accent */}
{/* header */}
/// SUN TECH & GEEK LTDA · documento legal
{content.title}
{content.subtitle}
{content.updated}
{/* scrollable body */}
{content.sections.map((s, i) =>
{s.title}
{s.body.map((p, j) =>
{p}
)}
)}
{/* footer actions */}
);
}
Object.assign(window, { Icon, LogoMark, OrbitalBackdrop, ServiceCard, QuoteModal, LegalModal, Field, PriceBlock, Term, renderWithTerms });