From f2cc99dd9476fc5f5ea35acfe413f4f69329a63f Mon Sep 17 00:00:00 2001 From: Pauljd Date: Wed, 30 Sep 2026 17:41:50 +0100 Subject: [PATCH] Initial trades demo starter --- README.md | 58 ++++ assets/icons/.gitkeep | 0 assets/images/.gitkeep | 0 assets/logo/.gitkeep | 0 css/base.css | 96 ++++++ css/components.css | 691 +++++++++++++++++++++++++++++++++++++++++ css/theme.css | 10 + data/business.json | 76 +++++ index.html | 141 +++++++++ js/main.js | 129 ++++++++ 10 files changed, 1201 insertions(+) create mode 100644 README.md create mode 100644 assets/icons/.gitkeep create mode 100644 assets/images/.gitkeep create mode 100644 assets/logo/.gitkeep create mode 100644 css/base.css create mode 100644 css/components.css create mode 100644 css/theme.css create mode 100644 data/business.json create mode 100644 index.html create mode 100644 js/main.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..7013755 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Local Service Demo Starter + +A small, reusable one-page website for local service businesses. It uses plain HTML, CSS, JavaScript, and JSON; there is no package manager, build step, or framework. + +## Run locally + +The page loads `data/business.json` with `fetch`, so open it through a local web server rather than directly as a `file://` URL. + +From this directory, run: + +```sh +python3 -m http.server 8000 +``` + +Then visit . + +## Make it yours + +1. Edit the business details, navigation, hero, services, about copy, and contact details in `data/business.json`. +2. Add a logo under `assets/logo/` and set `logo` and `logoAlt` in the JSON. Leave `logo` empty to use the business name as a text mark. +3. Replace `hero.image` with an image path in `assets/images/` (or a hosted image URL), and update `hero.imageAlt`. +4. Change the four `--brand-*` values at the top of `css/theme.css` to update the palette. + +The sample business and hero photo are demo content. Replace both before publishing. The sample photo is remotely hosted; use a local image if the demo needs to work without an internet connection. + +## Files + +- `index.html` contains the page structure. +- `css/base.css` contains resets, shared type, and layout foundations. +- `css/components.css` contains the page sections and responsive styles. +- `css/theme.css` contains the replaceable brand variables. +- `js/main.js` loads the JSON and fills the page content. +- `data/business.json` contains the sample business content. +- `assets/images/`, `assets/icons/`, and `assets/logo/` are for replaceable media. + +## Demo workflow + +This starter is designed for fast prospect demos. + +1. Clone the repository. +2. Replace the sample business data in `data/business.json`. +3. Add the prospect's logo and hero image. +4. Update the brand colours in `css/theme.css`. +5. Run the site locally. +6. Check mobile layout, links, contact details, and placeholders. +7. Run Davidson CI and Lighthouse. +8. Deploy to a temporary public URL. +9. Verify the live URL before sending it to the prospect. + +## Demo rules + +- Do not invent reviews, certifications, services, locations, or claims. +- Replace all sample content before publishing. +- Keep structural changes minimal unless the business genuinely needs them. + +## Goal + +A real business should be able to go from "yes, show me" to a personalised live demo in under 45 minutes. diff --git a/assets/icons/.gitkeep b/assets/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/logo/.gitkeep b/assets/logo/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..525d261 --- /dev/null +++ b/css/base.css @@ -0,0 +1,96 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 5rem; +} + +body { + margin: 0; + background: var(--brand-bg); + color: var(--brand-text); + font-family: var(--font-body); + font-size: 16px; + -webkit-font-smoothing: antialiased; +} + +img { + display: block; + max-width: 100%; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button:focus-visible, +a:focus-visible { + outline: 3px solid var(--brand-accent); + outline-offset: 4px; +} + +::selection { + background: var(--brand-accent); + color: var(--brand-primary); +} + +.page-width { + width: min(100% - 48px, 1200px); + margin-inline: auto; +} + +.section-pad { + padding-block: 112px; +} + +.eyebrow { + margin: 0 0 22px; + color: var(--brand-accent); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.13em; + line-height: 1.4; + text-transform: uppercase; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1, +h2 { + font-family: var(--font-display); +} + +@media (max-width: 640px) { + .page-width { + width: min(100% - 40px, 1200px); + } + + .section-pad { + padding-block: 76px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/css/components.css b/css/components.css new file mode 100644 index 0000000..9b080e3 --- /dev/null +++ b/css/components.css @@ -0,0 +1,691 @@ +.site-header { + position: absolute; + z-index: 5; + inset: 0 0 auto; + color: #fff; +} + +.header-inner { + display: flex; + min-height: 92px; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid rgb(255 255 255 / 24%); +} + +.brand, +.footer-brand { + display: inline-flex; + align-items: center; + gap: 12px; + font-family: var(--font-display); + font-size: 17px; + font-weight: 800; + letter-spacing: 0; +} + +.brand-logo { + width: 42px; + height: 42px; + object-fit: contain; +} + +.main-nav { + display: flex; + align-items: center; + gap: 36px; + margin-left: auto; + margin-right: 48px; + color: rgb(255 255 255 / 82%); + font-size: 13px; + font-weight: 500; +} + +.main-nav a, +.header-call, +.contact-email { + transition: color 160ms ease; +} + +.main-nav a:hover, +.contact-email:hover { + color: var(--brand-accent); +} + +.header-call { + display: grid; + gap: 3px; + text-align: right; +} + +.header-call-label { + color: rgb(255 255 255 / 65%); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.header-phone { + font-size: 14px; + font-weight: 700; +} + +.menu-toggle { + display: none; + width: 42px; + height: 42px; + padding: 12px 9px; + border: 1px solid rgb(255 255 255 / 45%); + background: transparent; + color: #fff; + cursor: pointer; +} + +.menu-toggle span { + display: block; + height: 1px; + margin: 5px 0; + background: currentColor; +} + +.hero { + position: relative; + display: flex; + min-height: min(760px, 100svh); + align-items: center; + overflow: hidden; + background: #292e2b; + color: #fff; +} + +.hero-image, +.hero-shade { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.hero-image { + object-fit: cover; + object-position: center 48%; + animation: hero-arrive 900ms ease-out both; +} + +.hero-shade { + background: rgb(13 20 20 / 62%); +} + +.hero-content { + position: relative; + z-index: 1; + padding-top: 108px; + padding-bottom: 92px; + animation: content-arrive 650ms 100ms ease-out both; +} + +.hero-eyebrow { + color: #ffb17c; +} + +.hero-title { + max-width: 790px; + margin-bottom: 22px; + font-size: clamp(48px, 7vw, 88px); + font-weight: 600; + letter-spacing: 0; + line-height: 0.99; +} + +.hero-description { + max-width: 500px; + margin-bottom: 32px; + color: rgb(255 255 255 / 78%); + font-size: 17px; + line-height: 1.7; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.button { + display: inline-flex; + min-height: 52px; + align-items: center; + justify-content: center; + padding: 0 23px; + font-size: 13px; + font-weight: 700; + transition: + background 160ms ease, + color 160ms ease, + border-color 160ms ease; +} + +.button-accent { + background: var(--brand-accent); + color: #171b1b; +} + +.button-accent:hover { + background: #ff9850; +} + +.button-outline { + border: 1px solid rgb(255 255 255 / 50%); + color: #fff; +} + +.button-outline:hover { + border-color: #fff; + background: rgb(255 255 255 / 10%); +} + +.hero-proof { + display: flex; + flex-wrap: wrap; + gap: 0; + margin-top: 64px; + color: rgb(255 255 255 / 85%); + font-size: 12px; +} + +.hero-proof span { + padding-inline: 18px; + border-left: 1px solid rgb(255 255 255 / 35%); +} + +.hero-proof span:first-child { + padding-left: 0; + border-left: 0; +} + +.hero-scroll { + position: absolute; + z-index: 1; + right: max(24px, calc((100vw - 1200px) / 2)); + bottom: 39px; + width: 22px; + height: 36px; + border: 1px solid rgb(255 255 255 / 65%); + border-radius: 12px; +} + +.hero-scroll span { + position: absolute; + top: 7px; + left: 9px; + width: 2px; + height: 7px; + background: var(--brand-accent); + animation: scroll-hint 1.6s infinite; +} + +.section-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 40px; + margin-bottom: 48px; +} + +.section-heading h2, +.about-title, +.contact-title { + margin-bottom: 0; + font-size: clamp(34px, 4vw, 52px); + font-weight: 600; + letter-spacing: 0; + line-height: 1.08; +} + +.section-heading .eyebrow, +.about-copy .eyebrow, +.contact-copy .eyebrow { + margin-bottom: 15px; +} + +.section-intro { + max-width: 360px; + margin: 0 0 3px; + color: var(--brand-muted); + font-size: 14px; + line-height: 1.75; +} + +.service-list { + display: grid; + grid-template-columns: repeat(3, 1fr); + border-top: 1px solid #d9d9d6; +} + +.service-item { + min-height: 210px; + padding: 25px 28px 26px 0; + border-bottom: 1px solid #d9d9d6; +} + +.service-item:not(:nth-child(3n + 1)) { + padding-left: 28px; + border-left: 1px solid #d9d9d6; +} + +.service-index { + display: block; + margin-bottom: 28px; + color: var(--brand-accent); + font-family: var(--font-display); + font-size: 12px; + font-weight: 800; +} + +.service-item h3 { + margin: 0 0 9px; + font-family: var(--font-display); + font-size: 19px; + font-weight: 700; +} + +.service-item p { + max-width: 310px; + margin: 0; + color: var(--brand-muted); + font-size: 13px; + line-height: 1.7; +} + +.about { + background: var(--brand-soft); +} + +.about-inner { + display: grid; + grid-template-columns: 0.45fr 1.4fr 0.9fr; + align-items: start; + gap: 54px; +} + +.about-marker { + display: flex; + align-items: center; + gap: 16px; + padding-top: 7px; + color: var(--brand-accent); + font-family: var(--font-display); + font-size: 13px; + font-weight: 800; +} + +.about-rule { + width: 48px; + height: 1px; + background: var(--brand-accent); +} + +.about-title { + max-width: 540px; +} + +.about-description { + max-width: 530px; + margin: 24px 0 20px; + color: #545a5c; + font-size: 15px; + line-height: 1.8; +} + +.text-link { + display: inline-flex; + align-items: center; + gap: 12px; + font-size: 13px; + font-weight: 700; +} + +.text-link::after { + width: 25px; + height: 1px; + background: var(--brand-accent); + content: ""; + transition: width 160ms ease; +} + +.text-link:hover::after { + width: 38px; +} + +.about-note { + position: relative; + margin-top: 43px; + padding: 28px 0 0 25px; + border-left: 1px solid #c7c5be; +} + +.note-mark { + position: absolute; + top: -17px; + left: 21px; + color: var(--brand-accent); + font-family: Georgia, serif; + font-size: 45px; + line-height: 1; +} + +.about-quote { + margin-bottom: 18px; + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + line-height: 1.65; +} + +.about-quote-byline { + color: var(--brand-muted); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.contact-inner { + display: grid; + grid-template-columns: 1.5fr 1fr 0.9fr; + align-items: center; + gap: 50px; +} + +.contact-title { + max-width: 540px; +} + +.contact-description { + max-width: 490px; + margin: 18px 0 0; + color: var(--brand-muted); + font-size: 14px; + line-height: 1.7; +} + +.contact-actions { + display: grid; + justify-items: start; + gap: 15px; +} + +.button-dark { + background: var(--brand-primary); + color: #fff; +} + +.button-dark:hover { + background: #2b3743; +} + +.contact-email { + font-size: 13px; + font-weight: 600; +} + +.contact-location { + display: grid; + gap: 8px; + padding-left: 26px; + border-left: 1px solid #d9d9d6; + font-size: 13px; + line-height: 1.5; +} + +.contact-location-label { + margin-bottom: 4px; + color: var(--brand-muted); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.contact-hours { + color: var(--brand-muted); +} + +.site-footer { + background: var(--brand-primary); + color: #fff; +} + +.footer-inner { + display: flex; + min-height: 88px; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.footer-brand { + font-size: 13px; +} + +.footer-location, +.footer-copyright { + margin: 0; + color: rgb(255 255 255 / 60%); + font-size: 11px; +} + +@keyframes hero-arrive { + from { + transform: scale(1.04); + } + to { + transform: scale(1); + } +} + +@keyframes content-arrive { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scroll-hint { + 0%, + 100% { + transform: translateY(0); + opacity: 1; + } + 65% { + transform: translateY(9px); + opacity: 0; + } +} + +@media (max-width: 900px) { + .header-inner { + min-height: 78px; + } + + .main-nav { + gap: 22px; + margin-right: 25px; + } + + .about-inner { + grid-template-columns: 0.25fr 1.3fr 0.85fr; + gap: 28px; + } + + .contact-inner { + grid-template-columns: 1.2fr 0.8fr; + gap: 36px; + } + + .contact-location { + grid-column: 2; + padding-top: 20px; + padding-left: 0; + border-top: 1px solid #d9d9d6; + border-left: 0; + } +} + +@media (max-width: 680px) { + .header-inner { + min-height: 72px; + } + + .brand { + position: relative; + z-index: 2; + max-width: calc(100% - 105px); + font-size: 15px; + } + + .brand-logo { + width: 34px; + height: 34px; + } + + .header-call { + display: none; + } + + .menu-toggle { + position: relative; + z-index: 2; + display: block; + } + + .main-nav { + position: absolute; + inset: 0 0 auto; + display: none; + align-items: stretch; + gap: 0; + margin: 0; + padding: 82px 24px 18px; + background: var(--brand-primary); + } + + .main-nav.is-open { + display: grid; + } + + .main-nav a { + padding: 15px 0; + border-top: 1px solid rgb(255 255 255 / 17%); + } + + .hero { + min-height: 700px; + min-height: max(680px, 100svh); + align-items: end; + } + + .hero-image { + object-position: 58% center; + } + + .hero-shade { + background: rgb(13 20 20 / 67%); + } + + .hero-content { + padding-top: 120px; + padding-bottom: 74px; + } + + .hero-title { + max-width: 500px; + font-size: clamp(46px, 13vw, 68px); + } + + .hero-description { + max-width: 420px; + font-size: 15px; + } + + .hero-proof { + gap: 12px 0; + margin-top: 42px; + } + + .hero-proof span { + padding-inline: 10px; + font-size: 10px; + } + + .hero-scroll { + right: 20px; + bottom: 23px; + } + + .section-heading { + display: block; + margin-bottom: 32px; + } + + .section-heading h2, + .about-title, + .contact-title { + font-size: 36px; + } + + .section-intro { + margin-top: 18px; + } + + .service-list { + grid-template-columns: 1fr; + } + + .service-item, + .service-item:not(:nth-child(3n + 1)) { + min-height: 0; + padding: 20px 0 22px; + border-left: 0; + } + + .service-index { + margin-bottom: 14px; + } + + .about-inner { + grid-template-columns: 1fr; + gap: 23px; + } + + .about-marker { + gap: 12px; + } + + .about-note { + max-width: 420px; + margin: 16px 0 0; + } + + .contact-inner { + grid-template-columns: 1fr; + gap: 28px; + } + + .contact-location { + grid-column: auto; + padding-top: 20px; + } + + .footer-inner { + min-height: 0; + align-items: flex-start; + flex-direction: column; + gap: 10px; + padding-block: 25px; + } +} diff --git a/css/theme.css b/css/theme.css new file mode 100644 index 0000000..7bcc766 --- /dev/null +++ b/css/theme.css @@ -0,0 +1,10 @@ +:root { + --brand-primary: #111827; + --brand-accent: #f97316; + --brand-bg: #ffffff; + --brand-text: #111111; + --brand-muted: #687078; + --brand-soft: #f3f1ed; + --font-body: "DM Sans", sans-serif; + --font-display: "Manrope", sans-serif; +} diff --git a/data/business.json b/data/business.json new file mode 100644 index 0000000..7ab1345 --- /dev/null +++ b/data/business.json @@ -0,0 +1,76 @@ +{ + "metaTitle": "Northline Electrical | Local Electrical Services", + "metaDescription": "Reliable electrical work, clear communication, and a tidy finish from Northline Electrical.", + "name": "Northline Electrical", + "homeLabel": "Northline Electrical home", + "logo": "", + "logoAlt": "", + "phone": "+1 503 555 0148", + "phoneDisplay": "(503) 555-0148", + "email": "hello@northline.example", + "location": "Portland, Oregon", + "hours": "Monday–Saturday, 7:30am–6pm", + "headerCallLabel": "Talk to an electrician", + "navigation": { + "services": "Services", + "about": "Our approach", + "contact": "Contact" + }, + "hero": { + "eyebrow": "Residential electrical · Portland, OR", + "title": "Good work.\nNo loose ends.", + "description": "Thoughtful electrical service for the projects that keep your home working beautifully.", + "image": "https://images.unsplash.com/photo-1621905251189-08b45d6a269e?auto=format&fit=crop&w=2200&q=85", + "imageAlt": "Electrician carefully working on a residential electrical installation", + "primaryAction": "Call to talk through your project", + "secondaryAction": "Explore our services" + }, + "highlights": ["Licensed & insured", "Up-front estimates", "Locally owned"], + "servicesSection": { + "eyebrow": "What we do", + "title": "The right fix,\ndone with care.", + "description": "From a small repair to a bigger upgrade, expect clear options and work done to last." + }, + "services": [ + { + "name": "Repairs & troubleshooting", + "description": "Get to the source of flickering lights, faulty outlets, tripped breakers, and other electrical issues." + }, + { + "name": "Lighting & fixtures", + "description": "Refresh a room with well-placed lighting, new fixtures, dimmers, and outdoor illumination." + }, + { + "name": "Panel upgrades", + "description": "Bring your electrical panel up to date and make room for the way your household uses power." + }, + { + "name": "EV charger installation", + "description": "Get a home charging setup planned and installed around your vehicle, panel, and parking." + }, + { + "name": "Remodels & additions", + "description": "Coordinate the wiring, circuits, and lighting needed to make your next space work." + }, + { + "name": "Safety inspections", + "description": "Understand the condition of your electrical system and what deserves attention first." + } + ], + "about": { + "number": "01", + "eyebrow": "A better service call", + "title": "Respect for your home is part of the job.", + "description": "We show up when we say we will, explain what we find, and leave the work area clean. You get straightforward recommendations and a team that treats your home like its own.", + "linkLabel": "Meet your local electrician", + "quote": "The work was excellent, the options were clear, and you wouldn't even know they'd been here.", + "quoteByline": "A note from a Portland homeowner" + }, + "contact": { + "eyebrow": "Tell us what you need", + "title": "Let's get it sorted.", + "description": "Call for a quick conversation about your project or send a note and we'll get back to you shortly.", + "phoneLabel": "Call (503) 555-0148", + "locationLabel": "Serving the greater area" + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..91cb1a7 --- /dev/null +++ b/index.html @@ -0,0 +1,141 @@ + + + + + + + + Local Service Business + + + + + + + + + + + +
+
+ +
+
+

+

+

+
+ + +
+
+
+ +
+ +
+
+
+
+

+

+
+

+
+
+
+
+ +
+
+
+ +
+
+

+

+

+ +
+
+ +

+ +
+
+
+ +
+
+
+

+

+

+
+
+ + +
+
+ + + +
+
+
+
+ +
+ +
+ + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..cf6bdb1 --- /dev/null +++ b/js/main.js @@ -0,0 +1,129 @@ +const text = (selector, value) => { + const element = document.querySelector(selector); + if (element) element.textContent = value ?? ""; +}; + +const setLink = (selector, href, label) => { + const element = document.querySelector(selector); + if (!element) return; + element.href = href; + element.textContent = 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 (business.logo) { + logo.src = business.logo; + logo.alt = business.logoAlt || business.name; + logo.hidden = false; + } + + document.querySelectorAll("[data-phone-link]").forEach((link) => { + link.href = `tel:${business.phone.replace(/[^\d+]/g, "")}`; + }); + setLink( + ".hero-primary", + `tel:${business.phone.replace(/[^\d+]/g, "")}`, + 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); + text(".hero-proof", ""); + const proof = document.querySelector(".hero-proof"); + business.highlights.forEach((item) => addText(proof, "span", "", item)); + + const heroImage = document.querySelector(".hero-image"); + heroImage.src = business.hero.image; + heroImage.alt = business.hero.imageAlt; + + text(".services-eyebrow", business.servicesSection.eyebrow); + text(".services-title", business.servicesSection.title); + text(".services-intro", business.servicesSection.description); + const serviceList = document.querySelector(".service-list"); + business.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); + addText(item, "p", "", service.description); + serviceList.append(item); + }); + + 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); + + text(".contact-eyebrow", business.contact.eyebrow); + text(".contact-title", business.contact.title); + text(".contact-description", business.contact.description); + const contactPhone = document.querySelector(".contact-phone"); + contactPhone.href = `tel:${business.phone.replace(/[^\d+]/g, "")}`; + contactPhone.textContent = 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}`); +}; + +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 = + '

Business details could not be loaded.

Run this site from a local web server and check data/business.json.

'; + });