From e821233ff65b631fb54c8fe4de21665b9a741cb7 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 12 Jul 2026 09:25:30 +0000 Subject: [PATCH] =?UTF-8?q?Footer=20=C3=BCberarbeitet:=20neues=20Design=20?= =?UTF-8?q?mit=20Flickering-Grid-Schriftzug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues Footer-Layout (Link-Spalten mit Chevron-Hover + animierter WEBklar-Schriftzug via FlickeringGrid). Inhalt (Branding, Seiten-, Kontakt- und Rechtliches-Links) aus dem bisherigen Footer übernommen. Co-Authored-By: Claude Opus 4.8 --- src/components/Footer.tsx | 183 +++++++++++-------- src/components/ui/flickering-grid.tsx | 247 ++++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 77 deletions(-) create mode 100644 src/components/ui/flickering-grid.tsx diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 969ab15..a4ddc22 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,91 +1,120 @@ import React from "react"; import { Link } from "react-router-dom"; +import { ChevronRight } from "lucide-react"; import Logo from "@/components/Logo"; +import { FlickeringGrid } from "@/components/ui/flickering-grid"; +import { useIsMobile } from "@/hooks/use-mobile"; + +type FooterLink = { + id: number; + title: string; + /** internal react-router route */ + to?: string; + /** anchor / mailto / tel / external */ + href?: string; +}; + +const footerLinks: { title: string; links: FooterLink[] }[] = [ + { + title: "Seiten", + links: [ + { id: 1, title: "Startseite", to: "/" }, + { id: 2, title: "Über uns", to: "/ueber-uns" }, + { id: 3, title: "Leistungen", href: "/#services" }, + { id: 4, title: "Kontakt", to: "/kontakt" }, + ], + }, + { + title: "Kontakt", + links: [ + { id: 5, title: "support@webklar.com", href: "mailto:support@webklar.com" }, + { id: 6, title: "0170 4969375", href: "tel:+491704969375" }, + ], + }, + { + title: "Rechtliches", + links: [ + { id: 7, title: "AGB", to: "/agb" }, + { id: 8, title: "Impressum", to: "/impressum" }, + ], + }, +]; const Footer: React.FC = () => { + const isMobile = useIsMobile(); + return ( -
-
-
-
- - - WEBklar - -
-

- Webagentur für KMU – Website, Prozesse und Automatisierung aus einer Hand. +

+
+
+ + + WEBklar + +

+ Webagentur für KMU – Website, Prozesse und Automatisierung aus einer + Hand.

-
© {new Date().getFullYear()} WEBklar. Alle Rechte vorbehalten.
+
+ © {new Date().getFullYear()} WEBklar. Alle Rechte vorbehalten. +
-
-
-

Seiten

-
    -
  • - - Startseite - -
  • -
  • - - Über uns - -
  • -
  • - - Leistungen - -
  • -
  • - - Kontakt - -
  • -
-
- -
-

Rechtliches

-
    -
  • - - AGB - -
  • -
  • - - Impressum - -
  • -
+
+
+ {footerLinks.map((column, columnIndex) => ( +
    +
  • + {column.title} +
  • + {column.links.map((link) => ( +
  • + {link.to ? ( + + {link.title} + + ) : ( + + {link.title} + + )} +
    + +
    +
  • + ))} +
+ ))}
-

- WEBklar -

-
+
+
+
+ +
+
+
); }; diff --git a/src/components/ui/flickering-grid.tsx b/src/components/ui/flickering-grid.tsx new file mode 100644 index 0000000..c42e86d --- /dev/null +++ b/src/components/ui/flickering-grid.tsx @@ -0,0 +1,247 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { cn } from "@/lib/utils"; + +// Parse a CSS color string ("#RRGGBB", "#RGB", "rgb(r,g,b)") into [r, g, b]. +// Keeps this component dependency-free (no color-bits needed) since the grid +// only ever receives a fixed hex color. +const parseColor = (color: string): [number, number, number] => { + const fallback: [number, number, number] = [180, 180, 180]; + if (!color) return fallback; + + const hex = color.trim(); + if (hex.startsWith("#")) { + let h = hex.slice(1); + if (h.length === 3) { + h = h + .split("") + .map((c) => c + c) + .join(""); + } + if (h.length === 6) { + const num = parseInt(h, 16); + return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; + } + return fallback; + } + + const rgbMatch = hex.match(/rgba?\(([^)]+)\)/); + if (rgbMatch) { + const parts = rgbMatch[1].split(",").map((p) => parseFloat(p.trim())); + if (parts.length >= 3) { + return [parts[0], parts[1], parts[2]]; + } + } + + return fallback; +}; + +interface FlickeringGridProps extends React.HTMLAttributes { + squareSize?: number; + gridGap?: number; + flickerChance?: number; + color?: string; + width?: number; + height?: number; + className?: string; + maxOpacity?: number; + text?: string; + fontSize?: number; + fontWeight?: number | string; +} + +export const FlickeringGrid: React.FC = ({ + squareSize = 3, + gridGap = 3, + flickerChance = 0.2, + color = "#B4B4B4", + width, + height, + className, + maxOpacity = 0.15, + text = "", + fontSize = 140, + fontWeight = 600, + ...props +}) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [isInView, setIsInView] = useState(false); + const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); + + const rgb = useMemo(() => parseColor(color), [color]); + + const drawGrid = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + cols: number, + rows: number, + squares: Float32Array, + dpr: number, + ) => { + ctx.clearRect(0, 0, width, height); + + const maskCanvas = document.createElement("canvas"); + maskCanvas.width = width; + maskCanvas.height = height; + const maskCtx = maskCanvas.getContext("2d", { willReadFrequently: true }); + if (!maskCtx) return; + + if (text) { + maskCtx.save(); + maskCtx.scale(dpr, dpr); + maskCtx.fillStyle = "white"; + maskCtx.font = `${fontWeight} ${fontSize}px "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`; + maskCtx.textAlign = "center"; + maskCtx.textBaseline = "middle"; + maskCtx.fillText(text, width / (2 * dpr), height / (2 * dpr)); + maskCtx.restore(); + } + + for (let i = 0; i < cols; i++) { + for (let j = 0; j < rows; j++) { + const x = i * (squareSize + gridGap) * dpr; + const y = j * (squareSize + gridGap) * dpr; + const squareWidth = squareSize * dpr; + const squareHeight = squareSize * dpr; + + const maskData = maskCtx.getImageData( + x, + y, + squareWidth, + squareHeight, + ).data; + const hasText = maskData.some( + (value, index) => index % 4 === 0 && value > 0, + ); + + const opacity = squares[i * rows + j]; + const finalOpacity = hasText + ? Math.min(1, opacity * 3 + 0.4) + : opacity; + + ctx.fillStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${finalOpacity})`; + ctx.fillRect(x, y, squareWidth, squareHeight); + } + } + }, + [rgb, squareSize, gridGap, text, fontSize, fontWeight], + ); + + const setupCanvas = useCallback( + (canvas: HTMLCanvasElement, width: number, height: number) => { + const dpr = window.devicePixelRatio || 1; + canvas.width = width * dpr; + canvas.height = height * dpr; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + const cols = Math.ceil(width / (squareSize + gridGap)); + const rows = Math.ceil(height / (squareSize + gridGap)); + + const squares = new Float32Array(cols * rows); + for (let i = 0; i < squares.length; i++) { + squares[i] = Math.random() * maxOpacity; + } + + return { cols, rows, squares, dpr }; + }, + [squareSize, gridGap, maxOpacity], + ); + + const updateSquares = useCallback( + (squares: Float32Array, deltaTime: number) => { + for (let i = 0; i < squares.length; i++) { + if (Math.random() < flickerChance * deltaTime) { + squares[i] = Math.random() * maxOpacity; + } + } + }, + [flickerChance, maxOpacity], + ); + + useEffect(() => { + const canvas = canvasRef.current; + const container = containerRef.current; + if (!canvas || !container) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animationFrameId: number; + let gridParams: ReturnType; + + const updateCanvasSize = () => { + const newWidth = width || container.clientWidth; + const newHeight = height || container.clientHeight; + setCanvasSize({ width: newWidth, height: newHeight }); + gridParams = setupCanvas(canvas, newWidth, newHeight); + }; + + updateCanvasSize(); + + let lastTime = 0; + const animate = (time: number) => { + if (!isInView) return; + + const deltaTime = (time - lastTime) / 1000; + lastTime = time; + + updateSquares(gridParams.squares, deltaTime); + drawGrid( + ctx, + canvas.width, + canvas.height, + gridParams.cols, + gridParams.rows, + gridParams.squares, + gridParams.dpr, + ); + animationFrameId = requestAnimationFrame(animate); + }; + + const resizeObserver = new ResizeObserver(() => { + updateCanvasSize(); + }); + + resizeObserver.observe(container); + + const intersectionObserver = new IntersectionObserver( + ([entry]) => { + setIsInView(entry.isIntersecting); + }, + { threshold: 0 }, + ); + + intersectionObserver.observe(canvas); + + if (isInView) { + animationFrameId = requestAnimationFrame(animate); + } + + return () => { + cancelAnimationFrame(animationFrameId); + resizeObserver.disconnect(); + intersectionObserver.disconnect(); + }; + }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]); + + return ( +
+ +
+ ); +}; + +export default FlickeringGrid;