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
+125
View File
@@ -0,0 +1,125 @@
import Link from "next/link";
import { Mail } from "lucide-react";
import { FaLinkedin } from "react-icons/fa6";
import { SiGitea } from "react-icons/si";
import type { Locale, Dictionary } from "@/lib/dictionaries";
import { LogoMark } from "@/components/ui/Logo";
interface FooterProps {
locale: Locale;
dict: Dictionary;
}
export default function Footer({ locale, dict }: FooterProps) {
const footerLinks = {
[dict.footer.product]: [
{ label: dict.nav.features, href: `/${locale}/#features` },
{ label: dict.nav.services, href: `/${locale}/services` },
{ label: dict.nav.contact, href: `/${locale}/contact` },
],
[dict.footer.company]: [
{ label: dict.nav.about, href: `/${locale}/about` },
],
[dict.footer.connect]: [
{ label: "Source Code", href: "https://git.ksan.dev/ksan", external: true },
{ label: "Email", href: "mailto:contact@ksan.dev", external: true },
{ label: "LinkedIn", href: "https://linkedin.com", external: true },
],
};
const socials = [
{ icon: SiGitea, href: "https://git.ksan.dev/ksan", label: "Gitea" },
{ icon: Mail, href: "mailto:contact@ksan.dev", label: "Email" },
{ icon: FaLinkedin, href: "https://linkedin.com", label: "LinkedIn" },
];
return (
<footer className="relative z-10 mt-32 border-t border-border/40">
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-purple-400/40 to-transparent" />
<div className="pointer-events-none absolute inset-x-0 top-0 h-64 overflow-hidden">
<div className="absolute left-1/2 top-0 h-64 w-[60rem] -translate-x-1/2 -translate-y-1/2 rounded-full bg-purple-500/15 blur-3xl" />
</div>
<div className="relative mx-auto max-w-7xl px-6 py-16 lg:px-8 lg:py-20">
<div className="grid grid-cols-2 gap-12 lg:grid-cols-5">
<div className="col-span-2 max-w-sm">
<Link href={`/${locale}`} className="inline-flex items-center gap-2.5">
<div className="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>
<span className="font-display text-base font-medium text-text-primary">
ksan.dev
</span>
</Link>
<p className="mt-5 text-sm leading-relaxed text-text-secondary">
Full-stack engineering, end to end. Everything built and hosted by me.
</p>
<div className="mt-6 flex items-center gap-3">
{socials.map((social) => (
<a
key={social.label}
href={social.href}
target={social.href.startsWith("http") ? "_blank" : undefined}
rel={social.href.startsWith("http") ? "noopener noreferrer" : undefined}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/50 text-text-secondary transition-all hover:border-purple-400/40 hover:bg-purple-500/10 hover:text-purple-300"
aria-label={social.label}
>
<social.icon className="h-4 w-4" />
</a>
))}
</div>
</div>
{Object.entries(footerLinks).map(([title, links]) => (
<div key={title}>
<h4 className="mb-4 font-mono text-xs uppercase tracking-widest text-text-secondary">
{title}
</h4>
<ul className="space-y-3">
{links.map((link) => (
<li key={link.label}>
{"external" in link && link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-text-secondary transition-colors hover:text-text-primary"
>
{link.label}
</a>
) : (
<Link
href={link.href}
className="text-sm text-text-secondary transition-colors hover:text-text-primary"
>
{link.label}
</Link>
)}
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-16 flex flex-col items-start gap-4 border-t border-border/40 pt-8 sm:flex-row sm:items-center sm:justify-between">
<p className="font-mono text-xs text-text-secondary">
© {new Date().getFullYear()} ksan.dev · {dict.footer.rights}
</p>
<p className="font-mono text-xs text-text-secondary">
<span className="inline-flex items-center gap-2">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inset-0 animate-ping rounded-full bg-purple-400 opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-purple-400" />
</span>
{dict.footer.status}
</span>
</p>
</div>
</div>
</footer>
);
}
+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>
</>
);
}