Files
pauljd 498f1d7092
pauljd/davidson-ci: Davidson CI / quality (push) Successful in 1m29s
pauljd/davidson-ci: Davidson CI / ai-review (push) Successful in 36s
Refine trades starter template
2026-10-01 11:59:07 +01:00

180 lines
6.5 KiB
JavaScript

const hasValue = (value) => typeof value === "string" && value.trim() !== "";
const setHidden = (selector, hidden) => {
document.querySelectorAll(selector).forEach((element) => {
element.hidden = hidden;
});
};
// Fills an element and hides it when the value is empty.
const text = (selector, value) => {
const element = document.querySelector(selector);
if (!element) return;
element.textContent = value ?? "";
element.hidden = !hasValue(value);
};
const setLink = (selector, href, label) => {
const element = document.querySelector(selector);
if (!element) return;
element.href = href;
element.textContent = label ?? "";
element.hidden = !hasValue(label);
};
const addText = (parent, tag, className, value) => {
const element = document.createElement(tag);
element.className = className;
element.textContent = value;
parent.append(element);
return element;
};
const renderBusiness = (business) => {
document.title = business.metaTitle;
document.querySelector('meta[name="description"]').content =
business.metaDescription;
text(".brand-name", business.name);
text(".footer-brand", business.name);
text(".header-call-label", business.headerCallLabel);
text(".header-phone", business.phoneDisplay);
text("[data-nav-services]", business.navigation.services);
text("[data-nav-about]", business.navigation.about);
text("[data-nav-contact]", business.navigation.contact);
const brand = document.querySelector(".brand");
brand.setAttribute("aria-label", business.homeLabel);
const logo = document.querySelector(".brand-logo");
if (hasValue(business.logo)) {
logo.src = business.logo;
logo.alt = business.logoAlt || business.name;
logo.hidden = false;
// A logo that fails to load falls back to the business name.
logo.addEventListener("error", () => {
logo.hidden = true;
});
}
const phoneHref = `tel:${(business.phone ?? "").replace(/[^\d+]/g, "")}`;
document.querySelectorAll("[data-phone-link]").forEach((link) => {
link.href = phoneHref;
});
setLink(".hero-primary", phoneHref, business.hero.primaryAction);
setLink(".hero-secondary", "#services", business.hero.secondaryAction);
text(".hero-eyebrow", business.hero.eyebrow);
text(".hero-title", business.hero.title);
text(".hero-description", business.hero.description);
const proof = document.querySelector(".hero-proof");
const highlights = (business.highlights ?? []).filter(hasValue);
proof.textContent = "";
highlights.forEach((item) => addText(proof, "span", "", item));
proof.hidden = highlights.length === 0;
// Without a usable image the hero keeps its plain dark background.
const heroImage = document.querySelector(".hero-image");
if (hasValue(business.hero.image)) {
heroImage.addEventListener("error", () => {
heroImage.hidden = true;
});
heroImage.src = business.hero.image;
heroImage.alt = business.hero.imageAlt ?? "";
} else {
heroImage.hidden = true;
}
text(".services-eyebrow", business.servicesSection.eyebrow);
text(".services-title", business.servicesSection.title);
text(".services-intro", business.servicesSection.description);
const serviceList = document.querySelector(".service-list");
const services = (business.services ?? []).filter((service) =>
hasValue(service.name),
);
services.forEach((service, index) => {
const item = document.createElement("article");
item.className = "service-item";
addText(item, "span", "service-index", String(index + 1).padStart(2, "0"));
addText(item, "h3", "", service.name);
if (hasValue(service.description))
addText(item, "p", "", service.description);
serviceList.append(item);
});
if (services.length === 0) {
setHidden(
"#services, [data-nav-services], .hero-secondary, .hero-scroll",
true,
);
}
text(".about-marker .about-number", business.about.number);
text(".about-eyebrow", business.about.eyebrow);
text(".about-title", business.about.title);
text(".about-description", business.about.description);
setLink(".about-link", "#contact", business.about.linkLabel);
text(".about-quote", business.about.quote);
text(".about-quote-byline", business.about.quoteByline);
// The quote treatment needs a quote; the section needs a title, a description, or a quote.
const hasQuote = hasValue(business.about.quote);
setHidden(".about-note", !hasQuote);
const hasAbout =
hasValue(business.about.title) ||
hasValue(business.about.description) ||
hasQuote;
if (!hasAbout) setHidden("#about, [data-nav-about]", true);
text(".contact-eyebrow", business.contact.eyebrow);
text(".contact-title", business.contact.title);
text(".contact-description", business.contact.description);
setLink(".contact-phone", phoneHref, business.contact.phoneLabel);
setLink(".contact-email", `mailto:${business.email ?? ""}`, business.email);
text(".contact-location-label", business.contact.locationLabel);
text(".contact-address", business.location);
text(".contact-hours", business.hours);
text(".footer-location", business.location);
text(".footer-copyright", `© ${new Date().getFullYear()} ${business.name}`);
if (!hasValue(business.phone)) {
setHidden("[data-phone-link], .hero-primary", true);
}
const hasLocationBlock =
hasValue(business.contact.locationLabel) ||
hasValue(business.location) ||
hasValue(business.hours);
setHidden(".contact-location", !hasLocationBlock);
};
const menuToggle = document.querySelector(".menu-toggle");
const navigation = document.querySelector(".main-nav");
menuToggle.addEventListener("click", () => {
const isOpen = menuToggle.getAttribute("aria-expanded") === "true";
menuToggle.setAttribute("aria-expanded", String(!isOpen));
menuToggle.setAttribute(
"aria-label",
isOpen ? "Open navigation" : "Close navigation",
);
navigation.classList.toggle("is-open", !isOpen);
});
navigation.addEventListener("click", (event) => {
if (event.target.closest("a")) {
navigation.classList.remove("is-open");
menuToggle.setAttribute("aria-expanded", "false");
menuToggle.setAttribute("aria-label", "Open navigation");
}
});
fetch("data/business.json")
.then((response) => {
if (!response.ok)
throw new Error(`Could not load business config: ${response.status}`);
return response.json();
})
.then(renderBusiness)
.catch((error) => {
console.error(error);
document.body.innerHTML =
'<main class="config-error"><h1>Business details could not be loaded.</h1><p>Run this site from a local web server and check data/business.json.</p></main>';
});