From a27b1fe25560bdffd49bd45d57dba3119fdac58f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 1 Oct 2026 10:39:46 +0000 Subject: [PATCH] Hero: ehrliche Fakten statt Zahlenleiste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Leiste mit Projektzähler (/api/projects), "Am Markt"-Ticker, 99,9 % Systemverfügbarkeit und Flagge ist ersetzt durch drei belegbare Fakten ohne Count-up: Falkenstein (Server in Deutschland), Live-Vorschau (im Kundenportal) und ab 20 € (Hosting im Monat). Einspaltig auf dem Handy, ab md drei Spalten. Der zweite Button "Projekte ansehen" ist entfernt, ebenso Projekt-Query, FOUNDING_DATE und src/lib/projects.ts. Co-Authored-By: Claude Sonnet 5.5 --- src/components/Hero.tsx | 130 ++++----------------------------- src/lib/projects.ts | 28 ------- src/test/sofort-paket.test.tsx | 119 ++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 144 deletions(-) delete mode 100644 src/lib/projects.ts create mode 100644 src/test/sofort-paket.test.tsx diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index a264f85..67e6689 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -1,25 +1,12 @@ import { useNavigate } from "react-router-dom"; import { ArrowRight } from "lucide-react"; -import React, { useState, useEffect, useRef, lazy, Suspense } from "react"; +import { lazy, Suspense } from "react"; import { useTheme } from "next-themes"; -import { useQuery } from "@tanstack/react-query"; -import CountUp from "@/components/CountUp"; import { trackCtaClick } from "@/lib/analytics"; -import { fetchProjects } from "@/lib/projects"; // WebGL-Effekte als eigene Chunks laden, damit der Text nicht auf three.js/ogl warten muss const Silk = lazy(() => import("@/components/Silk")); -const SPARKLE_SVG = ( - - - -); - function DemoButtonLetters({ text }: { text: string }) { const chars = text.split(""); return ( @@ -33,49 +20,16 @@ function DemoButtonLetters({ text }: { text: string }) { ); } -const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026 +// Drei belegbare Fakten statt Zahlen: Wort-Werte, deshalb kleiner gesetzt als die frühere Zahlenleiste. +const FAKTEN = [ + { wert: "Falkenstein", label: "Server in Deutschland" }, + { wert: "Live-Vorschau", label: "im Kundenportal" }, + { wert: "ab 20 €", label: "Hosting im Monat" }, +]; const Hero = () => { const navigate = useNavigate(); const { resolvedTheme } = useTheme(); - const [companyAge, setCompanyAge] = useState(""); - const secondBtnRef = useRef(null); - - // Projektzähler kommt live aus Gitea – gleicher queryKey wie im Abschnitt - // „Projekte", react-query teilt sich den einen Request. - const { data: projectsSnapshot } = useQuery({ - queryKey: ["projects"], - queryFn: ({ signal }) => fetchProjects(signal), - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - retry: 1, - }); - const projectCount = projectsSnapshot?.total ?? 10; - - useEffect(() => { - const calculateAge = () => { - const now = new Date(); - const diff = now.getTime() - FOUNDING_DATE.getTime(); - - const totalSeconds = Math.floor(diff / 1000); - const days = Math.floor(totalSeconds / (60 * 60 * 24)); - const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60)); - - const years = Math.floor(days / 365); - const remainingDays = days % 365; - - if (years > 0) { - setCompanyAge(`${years}J ${remainingDays}T ${hours}h`); - } else { - setCompanyAge(`${days}T ${hours}h`); - } - }; - - calculateAge(); - const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown) - - return () => clearInterval(interval); - }, []); // ── Silk-Hintergrund Farben ── const isDark = resolvedTheme === "dark"; @@ -149,32 +103,6 @@ const Hero = () => { -
- -
{/* Trust Line */} @@ -184,46 +112,16 @@ const Hero = () => { - {/* Bottom Stats Row */} + {/* Bottom Facts Row */}
-
-
-
- +
+ {FAKTEN.map((fakt, i) => ( +
+
{fakt.wert}
+
{fakt.label}
-
Projekte
-
-
-
{companyAge}
-
Am Markt
-
-
-
- -
-
Systemverfügbarkeit
-
-
-
🇩🇪
-
Serverstandort in Deutschland
-
+ ))}
diff --git a/src/lib/projects.ts b/src/lib/projects.ts deleted file mode 100644 index 0a41250..0000000 --- a/src/lib/projects.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Live-Projektzahl für die Statistik-Zeile im Hero. - * - * Quelle: GET /api/projects (contact-api-Container) – zählt serverseitig die - * Repositories, die aktuell in Gitea (git.webklar.com) liegen, angereichert um - * die Hosting-Metadaten aus Appwrite. Der Gitea-Token bleibt dadurch auf dem - * Server. Die Antwort enthält mehr Felder als hier genutzt werden; für die - * Homepage ist nur `total` relevant. - */ - -export type ProjectsSnapshot = { - /** Zeitpunkt, zu dem der Server-Snapshot erzeugt wurde (ISO) */ - generatedAt: string; - /** Anzahl der Repositories, die aktuell in Gitea existieren */ - total: number; - counts: Record; - source: string; -}; - -const API_URL = import.meta.env.VITE_PROJECTS_API ?? "/api/projects"; - -export async function fetchProjects(signal?: AbortSignal): Promise { - const res = await fetch(API_URL, { signal, headers: { Accept: "application/json" } }); - if (!res.ok) throw new Error(`Projektzahl konnte nicht geladen werden (HTTP ${res.status})`); - const data = (await res.json()) as ProjectsSnapshot; - if (typeof data?.total !== "number") throw new Error("Unerwartete Antwort der Projekt-API"); - return data; -} diff --git a/src/test/sofort-paket.test.tsx b/src/test/sofort-paket.test.tsx new file mode 100644 index 0000000..85b79ce --- /dev/null +++ b/src/test/sofort-paket.test.tsx @@ -0,0 +1,119 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, within, fireEvent, act, cleanup } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { AppRoutes } from "@/routes"; +import { usePageMeta } from "@/hooks/use-page-meta"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import Hero from "@/components/Hero"; +import ProblemSection from "@/components/ProblemSection"; +import SolutionSection from "@/components/SolutionSection"; +import Values from "@/components/Values"; +import Services from "@/components/Services"; +import Process from "@/components/Process"; +import KontaktAbschnitt from "@/components/Contact"; +import About from "@/pages/About"; +import { TextHoverEffect } from "@/components/ui/text-hover-effect"; +import { Toaster } from "@/components/ui/toaster"; + +// WebGL, Canvas und Netzwerk laden wir in jsdom nicht: die Effekt-Komponenten und +// das Kontaktformular-Backend werden durch Attrappen ersetzt. +vi.mock("@/components/Silk", () => ({ default: () => null })); +vi.mock("@/components/PixelBlast", () => ({ default: () => null })); +vi.mock("@/components/LightRays", () => ({ default: () => null })); +vi.mock("@/components/ui/flickering-grid", () => ({ FlickeringGrid: () => null })); +vi.mock("@/lib/appwrite", () => ({ createContactDocument: vi.fn().mockResolvedValue(undefined) })); + +// Dieselben Future-Flags wie der BrowserRouter in App.tsx, sonst warnt React Router in der Testausgabe. +const routerFuture = { v7_startTransition: true, v7_relativeSplatPath: true }; + +const DATENSCHUTZ = "https://project.webklar.com/datenschutz.html"; +const TELEFON = "0176 23726355"; +const TELEFON_HREF = "tel:+4917623726355"; + +/** Rendert die komplette Routenliste (mit gemeinsamem Footer) an einem Pfad. */ +const seite = (pfad: string) => + render( + + + , + ); + +/** Rendert eine einzelne Komponente im Router. */ +const zeige = (ui: React.ReactElement) => + render( + + {ui} + , + ); + +// ── Quelltext-Scan ───────────────────────────────────────────────────────────── +// vitest läuft im Projektverzeichnis +const WURZEL = `${process.cwd()}/`; +const dateien = (ordner: string): string[] => + readdirSync(ordner).flatMap((name) => { + const pfad = join(ordner, name); + return statSync(pfad).isDirectory() ? dateien(pfad) : [pfad]; + }); +const QUELLEN = [ + ...dateien(join(WURZEL, "src")).filter((p) => /\.(tsx?|css)$/.test(p) && !p.includes("/src/test/")), + join(WURZEL, "index.html"), +].map((pfad) => ({ pfad: pfad.replace(WURZEL, ""), text: readFileSync(pfad, "utf8") })); + +/** Pfade aller Quelldateien (ohne Tests), in denen das Muster vorkommt. */ +const vorkommen = (muster: RegExp) => QUELLEN.filter(({ text }) => muster.test(text)).map(({ pfad }) => pfad); +const lies = (pfad: string) => readFileSync(join(WURZEL, pfad), "utf8"); +const gibtEs = (pfad: string) => existsSync(join(WURZEL, pfad)); + +const meta = (name: string) => document.querySelector(`meta[name="${name}"]`)?.getAttribute("content"); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("B1 Hero: ehrliche Fakten statt Zahlenleiste", () => { + const FAKTEN = [ + ["Falkenstein", "Server in Deutschland"], + ["Live-Vorschau", "im Kundenportal"], + ["ab 20 €", "Hosting im Monat"], + ]; + + it("zeigt genau drei Fakten mit Wert und Beschriftung", () => { + zeige(); + for (const [wert, beschriftung] of FAKTEN) { + expect(screen.getByText(wert)).toBeInTheDocument(); + expect(screen.getByText(beschriftung)).toBeInTheDocument(); + } + const raster = screen.getByText("Falkenstein").closest(".grid"); + expect(raster?.children).toHaveLength(3); + expect(raster).toHaveClass("grid-cols-1", "md:grid-cols-3"); + }); + + it("die alte Zahlenleiste ist weg: kein Projektzähler, kein »Am Markt«, keine 99,9 %, keine Flagge", () => { + const { container } = zeige(); + expect(screen.queryByText("Am Markt")).toBeNull(); + expect(screen.queryByText("Projekte")).toBeNull(); + expect(screen.queryByText("Systemverfügbarkeit")).toBeNull(); + expect(container).not.toHaveTextContent(/99[,.]9|🇩🇪|Serverstandort/); + }); + + it("hat nur noch den Potenzialanalyse-Button, keinen »Projekte ansehen«", () => { + zeige(); + expect(screen.getAllByRole("button").map((b) => b.getAttribute("aria-label"))).toEqual([ + "Kostenlose Potenzialanalyse sichern", + ]); + }); + + it("lädt nichts vom Netz und braucht keinen QueryClient", () => { + const abruf = vi.fn(); + vi.stubGlobal("fetch", abruf); + zeige(); + expect(abruf).not.toHaveBeenCalled(); + expect(gibtEs("src/lib/projects.ts")).toBe(false); + expect(vorkommen(/api\/projects|fetchProjects|FOUNDING_DATE/)).toEqual([]); + }); +});