This repository has been archived on 2026-10-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
webklar-alt/src/components/Hero.tsx
T
rootandClaude Opus 5 60c827733f Hero: Projektzähler live statt hartkodierter 10+
Die Statistik-Zeile im Hero zeigte fest "10+ Projekte". Sie hängt jetzt
am selben /api/projects wie der Abschnitt "Projekte" (gleicher
react-query queryKey, also weiterhin nur ein Request) und zählt die
Repositories, die tatsächlich in Gitea liegen. Fallback bleibt 10,
falls die API nicht antwortet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:48:16 +00:00

227 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useNavigate } from "react-router-dom";
import { ArrowRight } from "lucide-react";
import React, { useState, useEffect, useRef } from "react";
import { useTheme } from "next-themes";
import { useQuery } from "@tanstack/react-query";
import Silk from "@/components/Silk";
import CountUp from "@/components/CountUp";
import { trackCtaClick } from "@/lib/analytics";
import { fetchProjects } from "@/lib/projects";
const SPARKLE_SVG = (
<svg className="btn-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
/>
</svg>
);
function DemoButtonLetters({ text }: { text: string }) {
const chars = text.split("");
return (
<>
{chars.map((char, i) => (
<span key={i} className={char === " " ? "btn-letter btn-letter-space" : "btn-letter"}>
{char}
</span>
))}
</>
);
}
const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
const Hero = () => {
const navigate = useNavigate();
const { resolvedTheme } = useTheme();
const [companyAge, setCompanyAge] = useState("");
const secondBtnRef = useRef<HTMLButtonElement>(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";
// Silk: Hauptfarbe (Wellenspitzen)
const silkColor = isDark ? "#6a6a6a" : "#ffffff";
// Silk: Zweite Farbe (Wellentäler)
const silkColor2 = isDark ? "#000000" : "#c0c0c0";
// Silk: Rausch-Intensität
const silkNoise = isDark ? 4 : 1.5;
return (
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden pt-20">
{/* Silk animated background */}
<div className="absolute inset-0 z-0 w-full h-full">
<Silk
speed={3}
scale={0.5}
color={silkColor}
color2={silkColor2}
noiseIntensity={silkNoise}
rotation={0}
/>
</div>
<div className="container mx-auto px-6 relative z-10">
<div className="max-w-6xl">
{/* Label */}
<div className="label-tag mb-8 animate-fade-in" style={{ animationDelay: '0.1s' }}>
Digitalisierung für KMU
</div>
{/* Main Heading - Large Uppercase */}
<h1 className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl xl:text-9xl font-display font-medium text-foreground mb-6 leading-[0.95] tracking-tighter uppercase animate-slide-up" style={{ animationDelay: '0.2s' }}>
Wir digitalisieren<br />
<span className="text-muted-foreground">Ihr Unternehmen</span>
</h1>
{/* Subheadline */}
<p className="text-xl md:text-2xl text-foreground/90 max-w-3xl mb-6 font-medium animate-fade-in" style={{ animationDelay: '0.3s' }}>
Website, Automatisierung und vernetzte Prozesse – aus einer Hand. So wächst Ihr Unternehmen, ohne dass Ihr Team mehr Aufwand im Alltag hat.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row flex-nowrap items-stretch sm:items-center gap-3 sm:gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}>
<div className="btn-wrapper shrink-0 w-full sm:w-auto">
<button
type="button"
className="btn btn-primary w-full sm:w-auto justify-center"
onClick={() => {
trackCtaClick("Kostenlose Potenzialanalyse sichern", "hero");
navigate("/kontakt");
}}
aria-label="Kostenlose Potenzialanalyse sichern"
>
<ArrowRight className="btn-icon" size={24} strokeWidth={2} aria-hidden />
<div className="txt-wrapper">
<span className="txt-width-helper" aria-hidden="true">
<DemoButtonLetters text="Kostenlose Potenzialanalyse sichern" />
</span>
<div className="txt-1">
<DemoButtonLetters text="Kostenlose Potenzialanalyse sichern" />
</div>
<div className="txt-2">
<DemoButtonLetters text="Wird weitergeleitet..." />
</div>
</div>
</button>
</div>
<div className="btn-wrapper w-full sm:w-auto">
<button
ref={secondBtnRef}
type="button"
className="btn w-full sm:w-auto justify-center"
onClick={() => {
const el = document.getElementById("projects");
if (el) el.scrollIntoView({ behavior: "smooth" });
else navigate("/#projects");
}}
aria-label="Projekte ansehen"
>
{SPARKLE_SVG}
<div className="txt-wrapper">
<span className="txt-width-helper" aria-hidden="true">
<DemoButtonLetters text="Projekte ansehen" />
</span>
<div className="txt-1">
<DemoButtonLetters text="Projekte ansehen" />
</div>
<div className="txt-2">
<DemoButtonLetters text="Wird geladen..." />
</div>
</div>
</button>
</div>
</div>
{/* Trust Line */}
<p className="text-sm text-muted-foreground/70 uppercase tracking-wider animate-fade-in" style={{ animationDelay: '0.55s' }}>
Für Unternehmen, die nicht stehen bleiben wollen.
</p>
</div>
</div>
{/* Bottom Stats Row */}
<div className="container mx-auto px-6 mt-auto pb-12 pt-20 relative z-10">
<div className="divider mb-12" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
<div className="animate-fade-in" style={{ animationDelay: '0.6s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">
<CountUp
from={0}
to={projectCount}
direction="up"
duration={1.2}
className="count-up-text"
startWhen={true}
suffix="+"
/>
</div>
<div className="label-tag">Projekte</div>
</div>
<div className="animate-fade-in" style={{ animationDelay: '0.7s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">{companyAge}</div>
<div className="label-tag">Am Markt</div>
</div>
<div className="animate-fade-in" style={{ animationDelay: '0.8s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">
<CountUp
from={0}
to={99.9}
direction="up"
duration={1.5}
className="count-up-text"
startWhen={true}
suffix="%"
/>
</div>
<div className="label-tag">Systemverfügbarkeit</div>
</div>
<div className="animate-fade-in" style={{ animationDelay: '0.9s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">🇩🇪</div>
<div className="label-tag">Serverstandort in Deutschland</div>
</div>
</div>
</div>
</section>
);
};
export default Hero;