// Main site sections — all copy comes from the Tweaks panel so it
// survives refresh and can be edited live.
const { useState, useEffect, useRef } = React;
// Turn a string with blank-line separated paragraphs into
blocks,
// running each through renderRich for *italic* support.
function paragraphs(text, className) {
if (!text) return null;
return String(text)
.split(/\n{2,}/)
.map((block, i) => (
{renderRich(block.replace(/\n/g, " "))}
));
}
function normalizePublicListing(listing) {
const status = listing.status || "new";
const statusClass = ["verkocht", "verkocht_ov", "sold"].includes(status)
? "sold"
: ["bezichtiging", "open"].includes(status)
? "open"
: "new";
const photos = Array.isArray(listing.fotos)
? listing.fotos
: Array.isArray(listing.photos)
? listing.photos
: [];
return {
id: listing.id,
title: listing.titel || listing.title || "Woning",
location: [listing.adres, listing.plaats || listing.location]
.filter(Boolean)
.join(" - "),
price: listing.prijsLabel || listing.price || "",
statusClass,
statusLabel:
listing.statusLabel ||
{
nieuw: "Nieuw in verkoop",
bezichtiging: "Bezichtiging",
verkocht: "Verkocht",
verkocht_ov: "Verkocht o.v.",
new: "Nieuw in verkoop",
open: "Bezichtiging",
sold: "Verkocht",
}[status] ||
"Nieuw in verkoop",
beds: listing.slaapkamers ?? listing.beds ?? 0,
baths: listing.badkamers ?? listing.baths ?? 0,
area: listing.woonoppervlak ?? listing.area ?? 0,
tone: listing.kleurToon || listing.tone || "warm",
image: photos[0] || listing.image || "",
};
}
// --- HEADER ------------------------------------------------------------
const Header = ({ onNavigate, t }) => {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 80);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
document.body.style.overflow = menuOpen ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [menuOpen]);
const go = (e, id) => {
e.preventDefault();
setMenuOpen(false);
onNavigate(id);
};
return (
);
};
// --- HERO --------------------------------------------------------------
const Hero = ({ onNavigate, tweaks = {} }) => {
const t = tweaks;
return (
{renderRich(t.heroTitle)}
{paragraphs(t.heroLede, "hero-lede")}
{t.showSign !== false && (t.heroSignName || t.heroSignMeta) && (
{t.heroSignName && {t.heroSignName} }
{t.heroSignMeta && {t.heroSignMeta} }
)}
{t.showScroll !== false && t.heroScrollLbl && (
{t.heroScrollLbl}
)}
);
};
// --- LISTINGS ----------------------------------------------------------
const Listings = ({ t }) => {
const visible = (window.LISTINGS_FROM_SERVER || window.LISTINGS || [])
.filter((listing) => listing.zichtbaar !== false)
.sort((a, b) => (a.volgorde || 0) - (b.volgorde || 0))
.map(normalizePublicListing);
return (
{t.aanbodEyebrow}
{renderRich(t.aanbodTitle)}
{visible.map((l) => (
{l.image ? (
) : (
)}
{l.statusLabel}
{l.location}
{l.title}
{l.price}
{l.beds} slaapk.
{l.baths} badk.
{l.area} m²
))}
);
};
// --- SERVICES ----------------------------------------------------------
const Services = ({ t }) => (
{t.dienstenEyebrow}
{renderRich(t.dienstenTitle)}
{paragraphs(t.dienstenIntro, "intro")}
{window.SERVICES.map((s) => (
{s.num}
{s.title}
{s.body}
{s.tags.map((tag) => (
{tag}
))}
))}
);
// --- PROCESS -----------------------------------------------------------
const Process = ({ t }) => (
{t.werkwijzeEyebrow}
{renderRich(t.werkwijzeTitle)}
{paragraphs(t.werkwijzeIntro, "intro")}
{window.PROCESS.map((s) => (
{s.num}
{s.title}
{s.body}
))}
);
// --- DOCUMENTS ---------------------------------------------------------
const DocIcon = ({ type }) => {
// Minimal file icons — clean line style
if (type === "pdf") {
return (
PDF
);
}
if (type === "doc") {
return (
DOC
);
}
return (
);
};
const Documents = ({ t }) => {
// Build doc list from tweaks (up to 6 documents)
const docs = [];
for (let i = 1; i <= 6; i++) {
const title = t[`doc${i}Title`];
if (!title) continue;
docs.push({
title,
desc: t[`doc${i}Desc`] || "",
type: t[`doc${i}Type`] || "pdf",
meta: t[`doc${i}Meta`] || "",
href: t[`doc${i}Href`] || "#",
});
}
if (docs.length === 0) return null;
return (
{t.docsEyebrow}
{renderRich(t.docsTitle)}
{paragraphs(t.docsIntro, "intro")}
);
};
// --- CONTACT -----------------------------------------------------------
const Contact = ({ t }) => {
const [form, setForm] = useState({
name: "",
email: "",
phone: "",
intent: "Verkoop",
message: "",
});
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const intents = ["Verkoop", "Aankoop", "Taxatie", "Hypotheek", "Iets anders"];
const validate = () => {
const e = {};
if (!form.name.trim()) e.name = "Vul uw naam in.";
if (!form.email.trim()) e.email = "Vul uw e-mailadres in.";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email))
e.email = "Dit e-mailadres klopt niet.";
if (!form.message.trim() || form.message.trim().length < 8)
e.message = "Schrijf kort waar u aan denkt.";
return e;
};
const submit = (ev) => {
ev.preventDefault();
const e = validate();
setErrors(e);
if (Object.keys(e).length === 0) {
setSubmitted(true);
fetch("/admin/api.php?action=inquiry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
}).catch(() => {});
}
};
const set = (k) => (ev) => {
setForm((f) => ({ ...f, [k]: ev.target.value }));
if (errors[k]) setErrors((er) => ({ ...er, [k]: undefined }));
};
return (
);
};
// --- FOOTER ------------------------------------------------------------
const Footer = ({ t }) => {
const year = new Date().getFullYear();
return (
{t.brandTitle}
Is een handelsnaam van:
Uw Paleis B.V.
KvK: 74025805
In samenwerking met Uw Hypotheek:
Uw Zekerheid B.V.
KvK: 32158765 ·{" "}
AFM: 12020092
Aangesloten bij
Vastgoed Nederland · MMCEPI
SCVM · NRVT · Hypokeur
© {year} {t.brandTitle}
);
};
Object.assign(window, {
Hero,
Listings,
Services,
Process,
Documents,
Contact,
Footer,
});