Compare commits

..

2 Commits

Author SHA1 Message Date
1d4584e5d9 fixes 2026-02-01 22:34:47 +01:00
6228945065 Added Silk component for animated background in Hero section 2026-02-01 16:59:54 +01:00
8 changed files with 2537 additions and 210 deletions

1879
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,8 @@
"@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@react-three/fiber": "^8.18.0",
"@tabler/icons-react": "^3.36.1",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -49,6 +51,7 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"motion": "^12.29.2",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
@@ -60,14 +63,15 @@
"sonner": "^1.7.4", "sonner": "^1.7.4",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
"vaul": "^0.9.9", "vaul": "^0.9.9",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/jest-dom": "^6.6.0", "@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5", "@types/node": "^22.16.5",
"@types/react": "^18.3.23", "@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",

View File

@@ -1,105 +1,97 @@
const Footer = () => { import React from 'react';
const currentYear = new Date().getFullYear();
const links = {
services: [
{ label: "Strategieberatung", href: "#services" },
{ label: "UX/UI Design", href: "#services" },
{ label: "Entwicklung", href: "#services" },
{ label: "SEO & Support", href: "#services" },
],
company: [
{ label: "Über uns", href: "#about" },
{ label: "Projekte", href: "#projects" },
{ label: "Ablauf", href: "#process" },
{ label: "Kontakt", href: "#contact" },
],
legal: [
{ label: "Impressum", href: "/impressum" },
{ label: "Datenschutz", href: "/datenschutz" },
{ label: "AGB", href: "/agb" },
],
};
const DevStudio: React.FC = () => {
return ( return (
<footer className="bg-secondary/20 border-t border-border relative"> <p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
<div className="container mx-auto px-6 py-16"> WEBklar
<div className="grid md:grid-cols-4 gap-12 mb-16"> </p>
{/* Logo & Description */}
<div className="md:col-span-1">
<span className="text-xl font-display font-medium text-foreground tracking-tight mb-6 block">
webklar
</span>
<p className="text-muted-foreground text-sm leading-relaxed">
Maßgeschneiderte Weblösungen für Ihr Unternehmen. Sicher, zuverlässig und modern.
</p>
</div>
{/* Services */}
<div>
<h4 className="label-tag mb-6">Leistungen</h4>
<ul className="space-y-3">
{links.services.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
{/* Company */}
<div>
<h4 className="label-tag mb-6">Unternehmen</h4>
<ul className="space-y-3">
{links.company.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
{/* Legal */}
<div>
<h4 className="label-tag mb-6">Rechtliches</h4>
<ul className="space-y-3">
{links.legal.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="divider mb-8" />
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-muted-foreground text-sm">
© {currentYear} webklar. Alle Rechte vorbehalten.
</p>
<div className="text-muted-foreground text-sm">
Made in Germany
</div>
</div>
</div>
</footer>
); );
}; };
const Footer: React.FC = () => {
return (
<div className="relative w-full overflow-hidden border-t border-white/[0.1] bg-black px-8 py-20 dark:border-white/[0.1] dark:bg-black">
<div className="mx-auto flex max-w-7xl flex-col items-start justify-between text-sm text-neutral-500 sm:flex-row md:px-8">
<div>
<div className="mr-0 mb-4 md:mr-4 md:flex">
<a className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-white" href="/">
<img alt="logo" width="30" height="30" src="https://assets.aceternity.com/logo-dark.png" />
<span className="font-medium text-white">WEBklar</span>
</a>
</div>
<div className="mt-2 ml-2">© copyright WEBklar 2024. All rights reserved.</div>
</div>
<div className="mt-10 grid grid-cols-2 items-start gap-10 sm:mt-0 md:mt-0 lg:grid-cols-4">
<div className="flex w-full flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Pages</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">All Products</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Studio</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Clients</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Pricing</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Blog</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Socials</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Facebook</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Instagram</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Twitter</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">LinkedIn</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Legal</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/agb">AGBs</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/impressum">Impressum</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Register</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Sign Up</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Login</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Forgot Password</a>
</li>
</ul>
</div>
</div>
</div>
<p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
WEBklar
</p>
</div>
);
};
export { DevStudio, Footer };
export default Footer; export default Footer;

View File

@@ -1,104 +1,98 @@
import { useState, useEffect } from "react"; "use client";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Menu, X } from "lucide-react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import {
Navbar,
NavBody,
NavItems,
MobileNav,
NavbarLogo,
NavbarButton,
MobileNavHeader,
MobileNavToggle,
MobileNavMenu,
} from "@/components/ui/resizable-navbar";
const Header = () => { const Header = () => {
const [isScrolled, setIsScrolled] = useState(false); const navItems = [
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); { name: "Über uns", link: "#about" },
{ name: "Leistungen", link: "#services" },
useEffect(() => { { name: "Projekte", link: "#projects" },
const handleScroll = () => { { name: "Ablauf", link: "#process" },
setIsScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navLinks = [
{ href: "#about", label: "Über uns" },
{ href: "#services", label: "Leistungen" },
{ href: "#projects", label: "Projekte" },
{ href: "#process", label: "Ablauf" },
]; ];
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
return ( return (
<header <div className="relative w-full">
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${ <Navbar>
isScrolled {/* Desktop Navigation */}
? "glass-nav py-4" <NavBody>
: "bg-transparent py-6" <NavbarLogo href="#">
}`} <span className="font-display text-lg font-medium tracking-tight">
> Webklar
<div className="container mx-auto px-6"> </span>
<div className="flex items-center justify-between"> </NavbarLogo>
{/* Logo */} <NavItems items={navItems} />
<a href="#" className="flex items-center gap-2 group"> <div className="navbar-actions flex items-center gap-4">
<span className="text-xl font-display font-medium text-foreground tracking-tight">Webklar</span>
</a>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-10">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors duration-300 text-sm font-medium uppercase tracking-wider"
>
{link.label}
</a>
))}
</nav>
{/* CTA Button */}
<div className="hidden md:block">
<Link to="/kontakt"> <Link to="/kontakt">
<Button <NavbarButton
className="btn-minimal rounded-full px-6 py-5 text-sm font-medium" as="span"
variant="primary"
className="!text-black"
> >
Kontakt Kontakt
</Button> </NavbarButton>
</Link> </Link>
</div> </div>
</NavBody>
{/* Mobile Menu Button */} {/* Mobile Navigation */}
<button <MobileNav>
className="md:hidden p-2 text-foreground hover:text-muted-foreground transition-colors" <MobileNavHeader>
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} <NavbarLogo href="#">
aria-label="Toggle menu" <span className="font-display text-lg font-medium tracking-tight">
Webklar
</span>
</NavbarLogo>
<MobileNavToggle
isOpen={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
/>
</MobileNavHeader>
<MobileNavMenu
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
> >
{isMobileMenuOpen ? ( {navItems.map((item, idx) => (
<X className="w-6 h-6" /> <a
) : ( key={`mobile-link-${idx}`}
<Menu className="w-6 h-6" /> href={item.link}
)} onClick={() => setIsMobileMenuOpen(false)}
</button> className="relative text-neutral-600 dark:text-neutral-300"
</div> >
<span className="block font-medium uppercase tracking-wider">
{/* Mobile Menu */} {item.name}
{isMobileMenuOpen && ( </span>
<div className="md:hidden absolute top-full left-0 right-0 bg-background/98 backdrop-blur-xl border-b border-border p-6 animate-fade-in"> </a>
<nav className="flex flex-col gap-6"> ))}
{navLinks.map((link) => ( <div className="flex w-full flex-col gap-4">
<a
key={link.href}
href={link.href}
className="text-foreground hover:text-muted-foreground transition-colors text-lg font-medium uppercase tracking-wider"
onClick={() => setIsMobileMenuOpen(false)}
>
{link.label}
</a>
))}
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}> <Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
<Button className="btn-minimal rounded-full mt-4 py-5 text-sm font-medium w-full"> <NavbarButton
as="span"
variant="primary"
className="block w-full text-center !text-black"
>
Kontakt Kontakt
</Button> </NavbarButton>
</Link> </Link>
</nav> </div>
</div> </MobileNavMenu>
)} </MobileNav>
</div> </Navbar>
</header> </div>
); );
}; };

View File

@@ -2,6 +2,7 @@ import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Silk from "@/components/Silk";
const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026 const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
@@ -16,28 +17,35 @@ const Hero = () => {
const totalSeconds = Math.floor(diff / 1000); const totalSeconds = Math.floor(diff / 1000);
const days = Math.floor(totalSeconds / (60 * 60 * 24)); const days = Math.floor(totalSeconds / (60 * 60 * 24));
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60)); const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = totalSeconds % 60;
const years = Math.floor(days / 365); const years = Math.floor(days / 365);
const remainingDays = days % 365; const remainingDays = days % 365;
if (years > 0) { if (years > 0) {
setCompanyAge(`${years}J ${remainingDays}T ${hours}h ${minutes}m ${seconds}s`); setCompanyAge(`${years}J ${remainingDays}T ${hours}h`);
} else { } else {
setCompanyAge(`${days}T ${hours}h ${minutes}m ${seconds}s`); setCompanyAge(`${days}T ${hours}h`);
} }
}; };
calculateAge(); calculateAge();
const interval = setInterval(calculateAge, 1000); // Update every second const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown)
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return ( return (
<section className="relative min-h-screen flex flex-col justify-center bg-background overflow-hidden pt-20"> <section className="relative min-h-screen flex flex-col justify-center overflow-hidden pt-20">
{/* Subtle grid lines */} {/* Silk animated background */}
<div className="absolute inset-0 grid-lines opacity-30" /> <div className="absolute inset-0 z-0 w-full h-full">
<Silk
speed={3}
scale={0.5}
color="#373737"
noiseIntensity={4
}
rotation={0}
/>
</div>
<div className="container mx-auto px-6 relative z-10"> <div className="container mx-auto px-6 relative z-10">
<div className="max-w-6xl"> <div className="max-w-6xl">
@@ -57,11 +65,6 @@ const Hero = () => {
Wir digitalisieren, automatisieren und vernetzen Ihre gesamte Firma in einem einzigen System damit Ihr Unternehmen wachsen kann, ohne dass Sie mehr arbeiten müssen. Wir digitalisieren, automatisieren und vernetzen Ihre gesamte Firma in einem einzigen System damit Ihr Unternehmen wachsen kann, ohne dass Sie mehr arbeiten müssen.
</p> </p>
{/* Kurztext */}
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mb-8 animate-fade-in" style={{ animationDelay: '0.4s' }}>
Die meisten Unternehmen arbeiten mit zu vielen Tools, manuellen Prozessen und ineffizienten Abläufen. Wir ersetzen Chaos durch Struktur und bauen Ihnen eine digitale Infrastruktur, die Zeit spart, Fehler reduziert und Wachstum planbar macht.
</p>
{/* CTA Buttons */} {/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}> <div className="flex flex-col sm:flex-row gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}>
<Link to="/kontakt"> <Link to="/kontakt">

158
src/components/Silk.tsx Normal file
View File

@@ -0,0 +1,158 @@
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
import { Color, type Mesh, type ShaderMaterial } from "three";
const hexToNormalizedRGB = (hex: string): [number, number, number] => {
hex = hex.replace("#", "");
return [
parseInt(hex.slice(0, 2), 16) / 255,
parseInt(hex.slice(2, 4), 16) / 255,
parseInt(hex.slice(4, 6), 16) / 255,
];
};
const vertexShader = `
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vPosition = position;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
varying vec3 vPosition;
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
const float e = 2.71828182845904523536;
float noise(vec2 texCoord) {
float G = e;
vec2 r = (G * sin(G * texCoord));
return fract(r.x * r.y * (1.0 + texCoord.x));
}
vec2 rotateUvs(vec2 uv, float angle) {
float c = cos(angle);
float s = sin(angle);
mat2 rot = mat2(c, -s, s, c);
return rot * uv;
}
void main() {
float rnd = noise(gl_FragCoord.xy);
vec2 uv = rotateUvs(vUv * uScale, uRotation);
vec2 tex = uv * uScale;
float tOffset = uSpeed * uTime;
tex.y += 0.03 * sin(8.0 * tex.x - tOffset);
float pattern = 0.6 +
0.4 * sin(5.0 * (tex.x + tex.y +
cos(3.0 * tex.x + 5.0 * tex.y) +
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
col.a = 1.0;
gl_FragColor = col;
}
`;
type SilkPlaneProps = {
uniforms: {
uSpeed: { value: number };
uScale: { value: number };
uNoiseIntensity: { value: number };
uColor: { value: Color };
uRotation: { value: number };
uTime: { value: number };
};
};
const SilkPlane = forwardRef<Mesh, SilkPlaneProps>(function SilkPlane(
{ uniforms },
ref
) {
const { viewport } = useThree();
useLayoutEffect(() => {
if (ref && typeof ref !== "function" && ref.current) {
ref.current.scale.set(viewport.width, viewport.height, 1);
}
}, [ref, viewport]);
useFrame((_, delta) => {
if (ref && typeof ref !== "function" && ref.current) {
const mat = ref.current.material as ShaderMaterial & {
uniforms: { uTime: { value: number } };
};
if (mat.uniforms?.uTime) mat.uniforms.uTime.value += 0.1 * delta;
}
});
return (
<mesh ref={ref}>
<planeGeometry args={[1, 1, 1, 1]} />
<shaderMaterial
uniforms={uniforms}
vertexShader={vertexShader}
fragmentShader={fragmentShader}
/>
</mesh>
);
});
SilkPlane.displayName = "SilkPlane";
type SilkProps = {
speed?: number;
scale?: number;
color?: string;
noiseIntensity?: number;
rotation?: number;
};
const Silk = ({
speed = 5,
scale = 1,
color = "#7B7481",
noiseIntensity = 1.5,
rotation = 0,
}: SilkProps) => {
const meshRef = useRef<Mesh>(null);
const uniforms = useMemo(
() => ({
uSpeed: { value: speed },
uScale: { value: scale },
uNoiseIntensity: { value: noiseIntensity },
uColor: { value: new Color(...hexToNormalizedRGB(color)) },
uRotation: { value: rotation },
uTime: { value: 0 },
}),
[speed, scale, noiseIntensity, color, rotation]
);
return (
<Canvas
dpr={[1, 2]}
frameloop="always"
style={{ width: "100%", height: "100%" }}
camera={{ position: [0, 0, 5], fov: 75 }}
>
<SilkPlane ref={meshRef} uniforms={uniforms} />
</Canvas>
);
};
export default Silk;

View File

@@ -0,0 +1,317 @@
"use client";
import { cn } from "@/lib/utils";
import { IconMenu2, IconX } from "@tabler/icons-react";
import {
motion,
AnimatePresence,
useScroll,
useMotionValueEvent,
} from "motion/react";
import React, { useRef, useState } from "react";
interface NavbarProps {
children: React.ReactNode;
className?: string;
}
export const Navbar = ({ children, className }: NavbarProps) => {
const ref = useRef<HTMLDivElement>(null);
const { scrollY } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const [visible, setVisible] = useState(false);
useMotionValueEvent(scrollY, "change", (latest) => {
if (latest > 100) {
setVisible(true);
} else {
setVisible(false);
}
});
return (
<motion.div
ref={ref}
className={cn("fixed inset-x-0 top-0 z-40 w-full", className)}
>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child, { visible } as { visible: boolean })
: child
)}
</motion.div>
);
};
interface NavBodyProps {
children: React.ReactNode;
className?: string;
visible?: boolean;
}
export const NavBody = ({ children, className, visible }: NavBodyProps) => {
return (
<motion.div
animate={{
backdropFilter: visible ? "blur(10px)" : "none",
boxShadow: visible
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
: "none",
width: visible ? "40%" : "100%",
y: visible ? 20 : 0,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 50,
}}
style={{
minWidth: "800px",
}}
className={cn(
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full bg-transparent px-4 py-2 lg:flex",
"text-white [&_a]:text-white [&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
visible && "bg-black/90",
className
)}
>
{children}
</motion.div>
);
};
interface NavItem {
name: string;
link: string;
}
interface NavItemsProps {
items: NavItem[];
className?: string;
onItemClick?: () => void;
}
export const NavItems = ({
items,
className,
onItemClick,
}: NavItemsProps) => {
const [hovered, setHovered] = useState<number | null>(null);
return (
<motion.div
onMouseLeave={() => setHovered(null)}
className={cn(
"absolute inset-0 hidden flex-1 flex-row items-center justify-center space-x-2 text-sm font-medium text-zinc-600 transition duration-200 hover:text-zinc-800 lg:flex lg:space-x-2",
className
)}
>
{items.map((item, idx) => (
<a
onMouseEnter={() => setHovered(idx)}
onClick={onItemClick}
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
key={`link-${idx}`}
href={item.link}
>
{hovered === idx && (
<motion.div
layoutId="hovered"
className="absolute inset-0 h-full w-full rounded-full bg-white/10"
/>
)}
<span className="relative z-20">{item.name}</span>
</a>
))}
</motion.div>
);
};
interface MobileNavProps {
children: React.ReactNode;
className?: string;
visible?: boolean;
}
export const MobileNav = ({
children,
className,
visible,
}: MobileNavProps) => {
return (
<motion.div
animate={{
backdropFilter: visible ? "blur(10px)" : "none",
boxShadow: visible
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
: "none",
width: visible ? "90%" : "100%",
paddingRight: visible ? "12px" : "0px",
paddingLeft: visible ? "12px" : "0px",
borderRadius: visible ? "4px" : "2rem",
y: visible ? 20 : 0,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 50,
}}
className={cn(
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
"[&>div:first-child]:text-white [&>div:first-child_a]:text-white [&>div:first-child_svg]:text-white",
visible && "bg-black/90",
className
)}
>
{children}
</motion.div>
);
};
interface MobileNavHeaderProps {
children: React.ReactNode;
className?: string;
}
export const MobileNavHeader = ({
children,
className,
}: MobileNavHeaderProps) => {
return (
<div
className={cn(
"flex w-full flex-row items-center justify-between",
className
)}
>
{children}
</div>
);
};
interface MobileNavMenuProps {
children: React.ReactNode;
className?: string;
isOpen: boolean;
onClose: () => void;
}
export const MobileNavMenu = ({
children,
className,
isOpen,
}: MobileNavMenuProps) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(
"absolute inset-x-0 top-16 z-50 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white px-4 py-8 shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset] dark:bg-neutral-950",
className
)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
interface MobileNavToggleProps {
isOpen: boolean;
onClick: () => void;
}
export const MobileNavToggle = ({ isOpen, onClick }: MobileNavToggleProps) => {
return isOpen ? (
<IconX
className="h-6 w-6 cursor-pointer text-black dark:text-white"
onClick={onClick}
/>
) : (
<IconMenu2
className="h-6 w-6 cursor-pointer text-black dark:text-white"
onClick={onClick}
/>
);
};
interface NavbarLogoProps {
href?: string;
logoSrc?: string;
logoAlt?: string;
children?: React.ReactNode;
className?: string;
}
export const NavbarLogo = ({
href = "#",
logoSrc,
logoAlt = "Logo",
children,
className,
}: NavbarLogoProps) => {
return (
<a
href={href}
className={cn(
"relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-black dark:text-white",
className
)}
>
{logoSrc ? (
<img src={logoSrc} alt={logoAlt} width={30} height={30} />
) : null}
{children}
</a>
);
};
interface NavbarButtonProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string;
as?: "a" | "button";
children: React.ReactNode;
className?: string;
variant?: "primary" | "secondary" | "dark" | "gradient";
}
export const NavbarButton = ({
href,
as: Tag = "a",
children,
className,
variant = "primary",
...props
}: NavbarButtonProps) => {
const baseStyles =
"px-4 py-2 rounded-md bg-white text-black text-sm font-bold relative cursor-pointer hover:-translate-y-0.5 transition duration-200 inline-block text-center";
const variantStyles = {
primary:
"shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
secondary: "bg-transparent shadow-none dark:text-white",
dark: "bg-black text-white shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
gradient:
"bg-gradient-to-b from-blue-500 to-blue-700 text-white shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]",
};
const componentProps =
Tag === "a"
? { href: href ?? undefined, ...props }
: { ...props };
return (
<Tag
className={cn(baseStyles, variantStyles[variant], className)}
{...(componentProps as React.ComponentProps<typeof Tag>)}
>
{children}
</Tag>
);
};

View File

@@ -17,7 +17,7 @@
@layer base { @layer base {
:root { :root {
/* Ultra Minimal Deep Black Theme - Muradov Inspired */ /* Ultra Minimal Deep Black Theme - Muradov Inspired */
--background: 0 0% 3%; --background: 0 0% 0%;
--foreground: 0 0% 92%; --foreground: 0 0% 92%;
--card: 0 0% 6%; --card: 0 0% 6%;