Initial commit

This commit is contained in:
2026-08-01 16:07:57 +02:00
commit 20b254e614
68 changed files with 8467 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, ArrowRight } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import type { Locale, Dictionary } from "@/lib/dictionaries";
import LanguageSwitcher from "@/components/ui/LanguageSwitcher";
import IPInfoCard from "@/components/ui/IPInfoCard";
import { LogoMark } from "@/components/ui/Logo";
interface NavbarProps {
locale: Locale;
dict: Dictionary;
}
export default function Navbar({ locale, dict }: NavbarProps) {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const pathname = usePathname();
const navLinks = [
{ href: `/${locale}/#features`, label: dict.nav.features },
{ href: `/${locale}/projects`, label: dict.nav.projects },
{ href: `/${locale}/services`, label: dict.nav.services },
{ href: `/${locale}/about`, label: dict.nav.about },
];
useEffect(() => {
const handler = () => setScrolled(window.scrollY > 12);
handler();
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [open]);
return (
<>
<motion.header
initial={{ y: -32, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
style={{ top: "var(--banner-h, 0px)" }}
className={cn(
"fixed inset-x-0 z-50 transition-all duration-500",
scrolled
? "border-b border-white/5 bg-background/70 backdrop-blur-xl"
: "bg-transparent",
)}
>
<nav className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6 lg:h-[72px] lg:px-8">
{/* Logo */}
<Link
href={`/${locale}`}
className="group flex items-center gap-2.5 transition-opacity hover:opacity-80"
>
<div className="relative flex h-8 w-8 items-center justify-center rounded-lg border border-purple-400/30 bg-purple-500/10 shadow-glow-sm">
<LogoMark className="w-[19px] text-white" />
<div className="absolute inset-0 rounded-lg bg-purple-500/20 opacity-0 blur-md transition-opacity group-hover:opacity-100" />
</div>
<span className="font-display text-base font-medium tracking-tight text-text-primary">
ksan.dev
</span>
</Link>
{/* Center: nav links + contact CTA — desktop */}
<div className="hidden items-center gap-1 md:flex">
{navLinks.map((link) => {
// Hash links (e.g. /en/#features) never appear in pathname,
// so they are deliberately excluded from active highlighting
const isActive =
!link.href.includes("#") &&
(pathname === link.href ||
pathname.startsWith(link.href + "/"));
return (
<Link
key={link.href}
href={link.href}
className={cn(
"relative rounded-lg px-4 py-2 text-sm transition-colors",
isActive
? "text-text-primary"
: "text-text-secondary hover:text-text-primary",
)}
>
{link.label}
{isActive && (
<motion.span
layoutId="nav-active"
className="absolute inset-0 -z-10 rounded-lg bg-white/5"
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
/>
)}
</Link>
);
})}
{/* Contact CTA — sits right next to About */}
<Link
href={`/${locale}/contact`}
className="group relative ml-1 inline-flex h-9 items-center gap-1.5 overflow-hidden rounded-xl bg-gradient-to-r from-purple-300 via-purple-500 to-purple-700 px-4 text-sm font-medium text-white shadow-glow-md transition-all hover:shadow-glow-lg hover:brightness-110"
>
<span
aria-hidden="true"
className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/25 to-transparent transition-transform duration-1000 group-hover:translate-x-full"
/>
<span className="relative z-10">{dict.nav.contact}</span>
<ArrowRight className="relative z-10 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
</Link>
</div>
{/* Right side: lang + IP — desktop */}
<div className="hidden items-center gap-2 md:flex">
<LanguageSwitcher locale={locale} />
<IPInfoCard dict={dict} />
</div>
{/* Mobile right side */}
<div className="flex items-center gap-2 md:hidden">
<LanguageSwitcher locale={locale} />
<IPInfoCard dict={dict} />
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="rounded-lg p-2 text-text-secondary hover:bg-white/5 hover:text-text-primary"
aria-label="Toggle menu"
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</nav>
</motion.header>
{/* Mobile menu */}
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40 bg-background/95 backdrop-blur-xl md:hidden"
>
<div className="flex h-full flex-col items-center justify-center gap-8 px-6 pt-16">
{navLinks.map((link, i) => (
<motion.div
key={link.href}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 * i, duration: 0.4 }}
>
<Link
href={link.href}
onClick={() => setOpen(false)}
className="font-display text-3xl font-medium tracking-tight text-text-primary transition-colors hover:text-purple-300"
>
{link.label}
</Link>
</motion.div>
))}
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.4 }}
>
<Link
href={`/${locale}/contact`}
onClick={() => setOpen(false)}
className="group inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-purple-300 via-purple-500 to-purple-700 px-6 py-3 font-display text-lg font-medium text-white shadow-glow-lg"
>
{dict.nav.contact}
<ArrowRight className="h-5 w-5" />
</Link>
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}