feat: add new contact form, hero, features, and richtext blocks; implement scroll-reveal action and update styles

- Introduced ContactFormBlock, FeaturesBlock, HeroBlock, and RichtextBlock components.
- Implemented a scroll-reveal action for animations on element visibility.
- Enhanced CSS styles for better theming and prose formatting.
- Added localization support for new components and updated existing translations.
- Created e2e tests for demo pages including contact form validation and navigation.
- Added a video tour showcasing the demo pages and interactions.
This commit is contained in:
2026-02-26 03:54:07 +00:00
parent e8fd38e98a
commit 40ffa8207e
27 changed files with 2009 additions and 98 deletions

View File

@@ -0,0 +1,166 @@
import { tour } from "./fixtures"
import { moveThenClick, moveThenType, smoothScroll } from "../helpers"
/**
* Video tour: Full demo showcase
*
* Walks through all demo pages, demonstrating:
* 1. Homepage — hero, features, richtext, FAQ accordion
* 2. About page — content blocks
* 3. Contact page — form interaction
* 4. Language switching (EN ↔ DE with route translation)
* 5. 404 page
*
* Run: yarn tour
*/
tour("Demo Showcase", async ({ tourPage: page }) => {
tour.setTimeout(120000)
// ── 1. Homepage (German) ─────────────────────────────────────────
await page.goto("/de/")
await page.waitForTimeout(2500)
// Scroll down through hero → features
await smoothScroll(page, 500)
await page.waitForTimeout(2000)
// Scroll through features
await smoothScroll(page, 1200)
await page.waitForTimeout(2500)
// Scroll to richtext/workflow section
await smoothScroll(page, 2000)
await page.waitForTimeout(2500)
// Scroll to FAQ accordion
await smoothScroll(page, 2800)
await page.waitForTimeout(2000)
// Interact with accordion — click second item
const accordionButtons = page.locator("[data-block='accordion'] button")
const secondAccordion = accordionButtons.nth(1)
if (await secondAccordion.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, secondAccordion)
await page.waitForTimeout(1500)
}
// Click third item
const thirdAccordion = accordionButtons.nth(2)
if (await thirdAccordion.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, thirdAccordion)
await page.waitForTimeout(1500)
}
// Scroll to footer
await smoothScroll(page, 9999)
await page.waitForTimeout(1500)
// Scroll back to top
await smoothScroll(page, 0)
await page.waitForTimeout(1500)
// ── 2. Navigate to About ─────────────────────────────────────────
const aboutLink = page.locator('header nav a[href*="ueber-uns"], header nav a[href*="about"]').first()
if (await aboutLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, aboutLink)
await page.waitForTimeout(2500)
// Slow scroll through about content
await smoothScroll(page, 400)
await page.waitForTimeout(2000)
await smoothScroll(page, 900)
await page.waitForTimeout(2000)
await smoothScroll(page, 1500)
await page.waitForTimeout(1500)
}
// ── 3. Navigate to Contact ───────────────────────────────────────
const contactLink = page.locator('header nav a[href*="kontakt"], header nav a[href*="contact"]').first()
if (await contactLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, contactLink)
await page.waitForTimeout(2500)
// Scroll to form
await smoothScroll(page, 500)
await page.waitForTimeout(1500)
// Fill out the contact form
const nameInput = page.getByLabel("Name", { exact: false })
if (await nameInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenType(page, nameInput, "Max Mustermann", { delay: 60 })
await page.waitForTimeout(800)
}
const emailInput = page.getByLabel("E-Mail", { exact: false }).or(page.getByLabel("Email", { exact: false }))
if (
await emailInput
.first()
.isVisible({ timeout: 2000 })
.catch(() => false)
) {
await moveThenType(page, emailInput.first(), "max@example.com", { delay: 60 })
await page.waitForTimeout(800)
}
// Select subject
const selectTrigger = page.locator("select, [role='combobox']").first()
if (await selectTrigger.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, selectTrigger)
await page.waitForTimeout(500)
await page.selectOption("select", { index: 2 })
await page.waitForTimeout(800)
}
const messageInput = page
.getByLabel("Nachricht", { exact: false })
.or(page.getByLabel("Message", { exact: false }))
if (
await messageInput
.first()
.isVisible({ timeout: 2000 })
.catch(() => false)
) {
await moveThenType(page, messageInput.first(), "Das ist eine Testnachricht für die Video-Tour.", {
delay: 40,
})
await page.waitForTimeout(1000)
}
// Submit form
const submitBtn = page
.locator("button[type='submit'], button:has-text('Senden'), button:has-text('Send')")
.first()
if (await submitBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, submitBtn)
await page.waitForTimeout(3000) // Wait for toast + simulated API call
}
}
// ── 4. Language switch ───────────────────────────────────────────
// Switch to English
const enLink = page.locator('header a[href*="/en"]').first()
if (await enLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, enLink)
await page.waitForTimeout(2500)
// Scroll a bit on English page
await smoothScroll(page, 300)
await page.waitForTimeout(1500)
await smoothScroll(page, 0)
await page.waitForTimeout(1000)
}
// ── 5. 404 page ─────────────────────────────────────────────────
await page.goto("/en/this-page-does-not-exist")
await page.waitForTimeout(2500)
// Click Back to Home
const backHomeLink = page.getByRole("link", { name: /back to home/i })
if (await backHomeLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await moveThenClick(page, backHomeLink)
await page.waitForTimeout(2500)
}
// Final pause on homepage
await page.waitForTimeout(2000)
})