Initial trades demo starter
This commit is contained in:
@@ -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 <http://localhost:8000>.
|
||||
|
||||
## 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.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Local service from a team you can trust."
|
||||
/>
|
||||
<title>Local Service Business</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="css/theme.css" />
|
||||
<link rel="stylesheet" href="css/base.css" />
|
||||
<link rel="stylesheet" href="css/components.css" />
|
||||
<script src="js/main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="header-inner page-width">
|
||||
<a class="brand" href="#top" aria-label="Home">
|
||||
<img class="brand-logo" alt="" hidden />
|
||||
<span class="brand-name"></span>
|
||||
</a>
|
||||
<nav class="main-nav" aria-label="Main navigation">
|
||||
<a data-nav-services href="#services"></a>
|
||||
<a data-nav-about href="#about"></a>
|
||||
<a data-nav-contact href="#contact"></a>
|
||||
</nav>
|
||||
<a class="header-call" data-phone-link>
|
||||
<span class="header-call-label"></span>
|
||||
<span class="header-phone"></span>
|
||||
</a>
|
||||
<button
|
||||
class="menu-toggle"
|
||||
type="button"
|
||||
aria-label="Open navigation"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top">
|
||||
<section class="hero" aria-labelledby="hero-title">
|
||||
<img class="hero-image" alt="" />
|
||||
<div class="hero-shade"></div>
|
||||
<div class="hero-content page-width">
|
||||
<p class="eyebrow hero-eyebrow"></p>
|
||||
<h1 id="hero-title" class="hero-title"></h1>
|
||||
<p class="hero-description"></p>
|
||||
<div class="hero-actions">
|
||||
<a class="button button-accent hero-primary"></a>
|
||||
<a class="button button-outline hero-secondary"></a>
|
||||
</div>
|
||||
<div class="hero-proof" aria-label="Business highlights"></div>
|
||||
</div>
|
||||
<a class="hero-scroll" href="#services" aria-label="Scroll to services"
|
||||
><span></span
|
||||
></a>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="services section-pad"
|
||||
id="services"
|
||||
aria-labelledby="services-title"
|
||||
>
|
||||
<div class="page-width">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow services-eyebrow"></p>
|
||||
<h2 id="services-title" class="services-title"></h2>
|
||||
</div>
|
||||
<p class="section-intro services-intro"></p>
|
||||
</div>
|
||||
<div class="service-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="about section-pad"
|
||||
id="about"
|
||||
aria-labelledby="about-title"
|
||||
>
|
||||
<div class="about-inner page-width">
|
||||
<div class="about-marker">
|
||||
<span class="about-number"></span><span class="about-rule"></span>
|
||||
</div>
|
||||
<div class="about-copy">
|
||||
<p class="eyebrow about-eyebrow"></p>
|
||||
<h2 id="about-title" class="about-title"></h2>
|
||||
<p class="about-description"></p>
|
||||
<a class="text-link about-link"></a>
|
||||
</div>
|
||||
<div class="about-note">
|
||||
<span class="note-mark" aria-hidden="true">“</span>
|
||||
<p class="about-quote"></p>
|
||||
<span class="about-quote-byline"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="contact section-pad"
|
||||
id="contact"
|
||||
aria-labelledby="contact-title"
|
||||
>
|
||||
<div class="contact-inner page-width">
|
||||
<div class="contact-copy">
|
||||
<p class="eyebrow contact-eyebrow"></p>
|
||||
<h2 id="contact-title" class="contact-title"></h2>
|
||||
<p class="contact-description"></p>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<a class="button button-dark contact-phone" data-phone-link></a>
|
||||
<a class="contact-email"></a>
|
||||
</div>
|
||||
<div class="contact-location">
|
||||
<span class="contact-location-label"></span>
|
||||
<span class="contact-address"></span>
|
||||
<span class="contact-hours"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner page-width">
|
||||
<a class="footer-brand" href="#top"></a>
|
||||
<p class="footer-location"></p>
|
||||
<p class="footer-copyright"></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+129
@@ -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 =
|
||||
'<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>';
|
||||
});
|
||||
Reference in New Issue
Block a user