feat: add localized language switcher and translate header component
All checks were successful
Deploy Portfolio to VPS / deploy (push) Successful in 29s

This commit is contained in:
Chneemann 2026-08-02 07:28:42 +02:00
parent 9e51dcf6c4
commit 5b261699a1
No known key found for this signature in database
3 changed files with 121 additions and 72 deletions

View file

@ -1,20 +1,26 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import { Link, usePathname } from "@/i18n/navigation";
import { useTranslations, useLocale } from "next-intl";
/** /**
* Main navigation header with desktop/mobile views and Command Palette trigger * Main navigation header with desktop/mobile views, Command Palette trigger and Language Switcher
*/ */
export default function Header() { export default function Header() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const tCommon = useTranslations("Common");
const tHeader = useTranslations("Header");
// Smooth scroll navigation targets const locale = useLocale();
const pathname = usePathname();
// Smooth scroll navigation targets mapped to translations
const navLinks = [ const navLinks = [
{ label: "About me", href: "#aboutme" }, { label: tCommon("about"), href: "#aboutme" },
{ label: "My Skills", href: "#myskills" }, { label: tCommon("skills"), href: "#myskills" },
{ label: "Portfolio", href: "#portfolio" }, { label: tCommon("portfolio"), href: "#portfolio" },
{ label: "Contact me", href: "#contactme" }, { label: tCommon("contact"), href: "#contactme" },
]; ];
// Dispatches global custom event to open Cmd+K palette // Dispatches global custom event to open Cmd+K palette
@ -22,6 +28,34 @@ export default function Header() {
window.dispatchEvent(new CustomEvent("toggle-cmd-k")); window.dispatchEvent(new CustomEvent("toggle-cmd-k"));
}; };
// Toggle between de and en while staying on the current page
const nextLocale = locale === "de" ? "en" : "de";
// Reusable language switcher
const LanguageSwitcher = () => (
<Link
href={pathname}
locale={nextLocale}
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-slate-900/80 border border-slate-800 text-slate-300 hover:text-white hover:border-slate-700 hover:bg-slate-900 transition-all font-mono text-xs shadow-xs cursor-pointer"
title="Switch Language"
>
<svg
className="w-3.5 h-3.5 text-blue-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{locale === "de" ? "EN" : "DE"}</span>
</Link>
);
return ( return (
<header className="fixed top-0 left-0 w-full bg-slate-950/60 backdrop-blur-md z-50 px-4 py-4 border-b border-slate-900/50"> <header className="fixed top-0 left-0 w-full bg-slate-950/60 backdrop-blur-md z-50 px-4 py-4 border-b border-slate-900/50">
<div className="max-w-5xl mx-auto flex justify-between items-center"> <div className="max-w-5xl mx-auto flex justify-between items-center">
@ -34,9 +68,9 @@ export default function Header() {
ANDRÉ KEMPF<span className="text-blue-500">.</span> ANDRÉ KEMPF<span className="text-blue-500">.</span>
</Link> </Link>
{/* Right Area: Desktop Nav & Cmd+K Trigger */} {/* Right Area: Desktop Nav, Language Switcher & Cmd+K Trigger */}
<div className="hidden md:flex items-center gap-8"> <div className="hidden md:flex items-center gap-6">
<nav className="flex gap-8 font-medium text-white text-sm"> <nav className="flex gap-6 font-medium text-white text-sm">
{navLinks.map((link) => ( {navLinks.map((link) => (
<Link <Link
key={link.href} key={link.href}
@ -48,62 +82,71 @@ export default function Header() {
))} ))}
</nav> </nav>
{/* Integrated Cmd+K Pill */} <div className="flex items-center gap-2">
<button <LanguageSwitcher />
onClick={triggerCmdK}
className="flex items-center gap-2 px-2.5 py-1 rounded-lg bg-slate-900/80 border border-slate-800 text-slate-400 hover:text-white hover:border-slate-700 hover:bg-slate-900 transition-all cursor-pointer font-mono text-xs shadow-xs" {/* Integrated Cmd+K Pill */}
title="Open command palette (⌘K)" <button
> onClick={triggerCmdK}
<svg className="flex items-center gap-2 px-2.5 py-1 rounded-lg bg-slate-900/80 border border-slate-800 text-slate-400 hover:text-white hover:border-slate-700 hover:bg-slate-900 transition-all cursor-pointer font-mono text-xs shadow-xs"
className="w-3.5 h-3.5 text-blue-400" title="Open command palette (⌘K)"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-3.5 h-3.5 text-blue-400"
strokeLinejoin="round" fill="none"
strokeWidth={2} stroke="currentColor"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" viewBox="0 0 24 24"
/> >
</svg> <path
<span className="text-[11px]">Search</span> strokeLinecap="round"
<kbd className="bg-slate-800 text-[10px] text-slate-300 px-1.5 py-0.5 rounded border border-slate-700/80 font-mono"> strokeLinejoin="round"
K strokeWidth={2}
</kbd> d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
</button> />
</svg>
<span className="text-[11px]">{tHeader("search")}</span>
<kbd className="bg-slate-800 text-[10px] text-slate-300 px-1.5 py-0.5 rounded border border-slate-700/80 font-mono">
K
</kbd>
</button>
</div>
</div> </div>
{/* Mobile Hamburger Toggle */} {/* Mobile Right Area: Language Switcher & Hamburger */}
<button <div className="flex items-center gap-3 md:hidden z-50">
onClick={(e) => { <LanguageSwitcher />
e.stopPropagation();
setIsOpen(!isOpen); {/* Mobile Hamburger Toggle */}
}} <button
className="md:hidden flex flex-col justify-center items-center w-8 h-8 space-y-1.5 z-50 focus:outline-none cursor-pointer group" onClick={(e) => {
aria-label="Toggle Menu" e.stopPropagation();
> setIsOpen(!isOpen);
<span }}
className={`block w-6 h-0.5 bg-slate-100 transition-all duration-300 group-hover:bg-blue-400 ${ className="flex flex-col justify-center items-center w-8 h-8 space-y-1.5 focus:outline-none cursor-pointer group"
isOpen ? "rotate-45 translate-y-2" : "" aria-label="Toggle Menu"
}`} >
/> <span
<span className={`block w-6 h-0.5 bg-slate-100 transition-all duration-300 group-hover:bg-blue-400 ${
className={`block w-6 h-0.5 bg-slate-100 transition-all duration-300 group-hover:bg-blue-400 ${ isOpen ? "rotate-45 translate-y-2" : ""
isOpen ? "opacity-0" : "" }`}
}`} />
/> <span
<span className={`block w-6 h-0.5 bg-slate-100 transition-all duration-300 group-hover:bg-blue-400 ${
className={`block w-6 h-0.5 bg-slate-100 transition-all duration-300 group-hover:bg-blue-400 ${ isOpen ? "opacity-0" : ""
isOpen ? "-rotate-45 -translate-y-2" : "" }`}
}`} />
/> <span
</button> className={`block w-6 h-0.5 bg-slate-100 transition-all duration-300 group-hover:bg-blue-400 ${
isOpen ? "-rotate-45 -translate-y-2" : ""
}`}
/>
</button>
</div>
{/* Mobile Fullscreen Navigation Overlay */} {/* Mobile Fullscreen Navigation Overlay */}
<div <div
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className={`fixed inset-0 w-full h-screen bg-slate-950/98 flex flex-col items-center justify-center gap-8 text-2xl font-semibold transition-all duration-300 md:hidden ${ className={`fixed inset-0 w-full h-screen bg-slate-950/98 flex flex-col items-center justify-center gap-8 text-2xl font-semibold transition-all duration-300 md:hidden z-40 ${
isOpen isOpen
? "opacity-100 pointer-events-auto" ? "opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none" : "opacity-0 pointer-events-none"
@ -113,22 +156,12 @@ export default function Header() {
<Link <Link
key={link.href} key={link.href}
href={link.href} href={link.href}
onClick={() => setIsOpen(false)}
className="hover:text-blue-400 text-white transition-colors" className="hover:text-blue-400 text-white transition-colors"
> >
{link.label} {link.label}
</Link> </Link>
))} ))}
{/* Mobile Cmd+K Trigger inside Overlay */}
<button
onClick={triggerCmdK}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-300 text-sm font-mono mt-4"
>
<span>Open Commands</span>
<kbd className="bg-slate-800 text-xs px-2 py-0.5 rounded border border-slate-700">
K
</kbd>
</button>
</div> </div>
</div> </div>
</header> </header>

View file

@ -1,3 +1,11 @@
{ {
"Common": {} "Common": {
"about": "Über mich",
"skills": "Fähigkeiten",
"portfolio": "Portfolio",
"contact": "Kontakt"
},
"Header": {
"search": "Suchen"
}
} }

View file

@ -1,3 +1,11 @@
{ {
"Common": {} "Common": {
"about": "About me",
"skills": "My Skills",
"portfolio": "Portfolio",
"contact": "Contact me"
},
"Header": {
"search": "Search"
}
} }