feat: add localized language switcher and translate header component
All checks were successful
Deploy Portfolio to VPS / deploy (push) Successful in 29s
All checks were successful
Deploy Portfolio to VPS / deploy (push) Successful in 29s
This commit is contained in:
parent
9e51dcf6c4
commit
5b261699a1
3 changed files with 121 additions and 72 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
{
|
{
|
||||||
"Common": {}
|
"Common": {
|
||||||
|
"about": "Über mich",
|
||||||
|
"skills": "Fähigkeiten",
|
||||||
|
"portfolio": "Portfolio",
|
||||||
|
"contact": "Kontakt"
|
||||||
|
},
|
||||||
|
"Header": {
|
||||||
|
"search": "Suchen"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
{
|
{
|
||||||
"Common": {}
|
"Common": {
|
||||||
|
"about": "About me",
|
||||||
|
"skills": "My Skills",
|
||||||
|
"portfolio": "Portfolio",
|
||||||
|
"contact": "Contact me"
|
||||||
|
},
|
||||||
|
"Header": {
|
||||||
|
"search": "Search"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue