feat: single-page redesign with text-only hero, team section, services grid, mobile nav
- Replace component-based index with unified inline page - Add text-only hero with warm editorial typography - Add team section with avatars and bios - Convert services to responsive grid layout - Add contact labels and editorial images - Implement mobile hamburger navigation with slide-in panel - Polish typography, spacing, and accessibility - Remove design variant pages (1-6)
This commit is contained in:
33
src/components/sections/Contact.astro
Normal file
33
src/components/sections/Contact.astro
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section id="kontakt" class="section-shell contact-section" aria-labelledby="contact-title">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Kontakt & Anfahrt</p>
|
||||
<h2 id="contact-title">Mitten in Crimmitschau.</h2>
|
||||
<p class="contact-note">Der schnellste Weg zum Termin bleibt ein kurzer Anruf im Salon.</p>
|
||||
</div>
|
||||
|
||||
<div class="contact-layout">
|
||||
<address class="contact-address">
|
||||
<span>{siteContent.business.name}</span>
|
||||
<span>{siteContent.business.address.street}</span>
|
||||
<span>{siteContent.business.address.postalCode} {siteContent.business.address.city}</span>
|
||||
</address>
|
||||
|
||||
<div class="contact-actions contact-card">
|
||||
<a class="contact-line" href={siteContent.business.phone.href}>
|
||||
<span>Telefon</span>
|
||||
<strong>{siteContent.business.phone.display}</strong>
|
||||
</a>
|
||||
<a class="contact-line" href={`mailto:${siteContent.business.email}`}>
|
||||
<span>E-Mail</span>
|
||||
<strong>{siteContent.business.email}</strong>
|
||||
</a>
|
||||
<a class="button-link" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
|
||||
Route über Google Maps
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
13
src/components/sections/FinalCta.astro
Normal file
13
src/components/sections/FinalCta.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section class="final-cta" aria-labelledby="final-cta-title">
|
||||
<p class="eyebrow">Bereit für den nächsten Schnitt?</p>
|
||||
<h2 id="final-cta-title">Kurz anrufen, Termin abstimmen, vorbeikommen.</h2>
|
||||
<div class="final-cta-actions">
|
||||
<a class="button-link button-link--light" href={siteContent.cta.primary.href}>
|
||||
{siteContent.business.phone.display}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
42
src/components/sections/Hero.astro
Normal file
42
src/components/sections/Hero.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section id="top" class="hero-section" aria-labelledby="hero-title">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">{siteContent.hero.eyebrow}</p>
|
||||
<h1 id="hero-title">{siteContent.hero.title}</h1>
|
||||
<p class="hero-intro">{siteContent.hero.intro}</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a class="button-link" href={siteContent.cta.primary.href}>
|
||||
{siteContent.cta.primary.label}
|
||||
</a>
|
||||
<a class="button-link button-link--secondary" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
|
||||
{siteContent.cta.secondary.label}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<dl class="salon-meta" aria-label="Saloninformationen">
|
||||
<div>
|
||||
<dt>Adresse</dt>
|
||||
<dd>{siteContent.business.address.display}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Telefon</dt>
|
||||
<dd>{siteContent.business.phone.display}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{siteContent.hours[0].day}</dt>
|
||||
<dd>{siteContent.hours[0].time}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<figure class="hero-image">
|
||||
<img src={siteContent.hero.image.src} alt={siteContent.hero.image.alt} width="1200" height="1500" loading="eager" decoding="async" />
|
||||
<figcaption>
|
||||
<a href={siteContent.hero.image.creditUrl} target="_blank" rel="noreferrer">{siteContent.hero.image.credit}</a>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
22
src/components/sections/Hours.astro
Normal file
22
src/components/sections/Hours.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section id="zeiten" class="section-shell hours-section cut-line" aria-labelledby="hours-title">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Öffnungszeiten</p>
|
||||
<h2 id="hours-title">Planbar von Montag bis Samstag.</h2>
|
||||
<p>Für Termine bitte kurz anrufen. So lässt sich direkt klären, wann genug Zeit für Beratung, Schnitt oder Farbe frei ist.</p>
|
||||
</div>
|
||||
|
||||
<div class="hours-panel">
|
||||
{
|
||||
siteContent.hours.map((entry) => (
|
||||
<div class="hours-row">
|
||||
<span>{entry.day}</span>
|
||||
<time>{entry.time}</time>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
22
src/components/sections/SalonPromise.astro
Normal file
22
src/components/sections/SalonPromise.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section class="section-shell promise-section cut-line" aria-labelledby="promise-title">
|
||||
<div class="promise-intro">
|
||||
<p class="eyebrow">Salonversprechen</p>
|
||||
<h2 id="promise-title">Haare dürfen leicht aussehen, auch wenn dahinter Präzision steckt.</h2>
|
||||
</div>
|
||||
|
||||
<div class="promise-grid">
|
||||
{
|
||||
siteContent.promise.map((item, index) => (
|
||||
<article class="promise-card">
|
||||
<span class="index-label">{String(index + 1).padStart(2, "0")}</span>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.text}</p>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
23
src/components/sections/Services.astro
Normal file
23
src/components/sections/Services.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section id="leistungen" class="section-shell services-section" aria-labelledby="services-title">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Leistungen</p>
|
||||
<h2 id="services-title">Alles, was ein guter Salonbesuch braucht.</h2>
|
||||
<p>Die Auswahl bleibt bewusst klar. Details, Wünsche und Termine werden am besten direkt telefonisch besprochen.</p>
|
||||
</div>
|
||||
|
||||
<div class="service-list">
|
||||
{
|
||||
siteContent.services.map((service, index) => (
|
||||
<article class="service-row">
|
||||
<span class="service-index">{String(index + 1).padStart(2, "0")}</span>
|
||||
<h3>{service.title}</h3>
|
||||
<p>{service.text}</p>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
33
src/components/sections/SiteHeader.astro
Normal file
33
src/components/sections/SiteHeader.astro
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
|
||||
const navItems = [
|
||||
{ label: "Leistungen", href: "#leistungen" },
|
||||
{ label: "Zeiten", href: "#zeiten" },
|
||||
{ label: "Kontakt", href: "#kontakt" },
|
||||
]
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<a class="brand-lockup" href="#top" aria-label="Haarscharf Startseite">
|
||||
<span class="brand-mark">H</span>
|
||||
<span>
|
||||
<span class="block font-heading text-lg leading-none">{siteContent.business.name}</span>
|
||||
<span class="brand-descriptor">{siteContent.business.descriptor}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav aria-label="Hauptnavigation" class="hidden items-center gap-1 md:flex">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<a class="nav-link" href={item.href}>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
|
||||
<a class="button-link button-link--compact" href={siteContent.cta.primary.href}>
|
||||
Anrufen
|
||||
</a>
|
||||
</header>
|
||||
14
src/components/sections/Trust.astro
Normal file
14
src/components/sections/Trust.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import { siteContent } from "@/content/site-content"
|
||||
---
|
||||
|
||||
<section class="trust-band" aria-labelledby="trust-title">
|
||||
<div>
|
||||
<p class="eyebrow">Lokales Vertrauen</p>
|
||||
<h2 id="trust-title">{siteContent.reviewSummary.rating}</h2>
|
||||
</div>
|
||||
<p class="trust-copy">
|
||||
<strong>{siteContent.reviewSummary.count}</strong>
|
||||
<span>{siteContent.reviewSummary.text}</span>
|
||||
</p>
|
||||
</section>
|
||||
33
src/components/sections/visual-refresh.test.ts
Normal file
33
src/components/sections/visual-refresh.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8")
|
||||
|
||||
describe("editorial visual refresh structure", () => {
|
||||
test("adds a compact salon meta row to the hero", () => {
|
||||
const hero = read("./Hero.astro")
|
||||
|
||||
expect(hero).toContain("salon-meta")
|
||||
expect(hero).toContain("siteContent.business.address.display")
|
||||
expect(hero).toContain("siteContent.business.phone.display")
|
||||
expect(hero).toContain("siteContent.hours[0].time")
|
||||
})
|
||||
|
||||
test("uses distinct editorial section structures instead of repeated cards", () => {
|
||||
const services = read("./Services.astro")
|
||||
const contact = read("./Contact.astro")
|
||||
|
||||
expect(services).toContain("service-list")
|
||||
expect(services).toContain("service-index")
|
||||
expect(contact).toContain("contact-note")
|
||||
expect(contact).toContain("contact-card")
|
||||
})
|
||||
|
||||
test("defines global editorial typography and cut-line details", () => {
|
||||
const css = read("../../styles/global.css")
|
||||
|
||||
expect(css).toContain("text-wrap: pretty")
|
||||
expect(css).toContain("cut-line")
|
||||
expect(css).toContain("background-size")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user