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:
2026-05-17 17:40:50 +02:00
parent 3b34a7b2a4
commit 90c8762eb7
18 changed files with 2067 additions and 51 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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")
})
})