-
-
-
+
+ {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([]);
+ });
+});