feat: migrate to Next.js 15 App Router with i18n and multi-page SEO
- Replace Vite+React SPA with Next.js 15, TypeScript, App Router - Static export (output: 'export'), deploy via npm run deploy - i18n routing: /ru/... and /en/... with generateStaticParams - New pages: /uslugi/ (catalog), /uslugi/[slug]/ (8 service pages), /faq/, /kontakty/ - SEO: generateMetadata on all pages, JSON-LD Service schema, hreflang alternates - ContactForm: 'use client', Turnstile + POST to /api/contact.php - Fonts: Manrope via next/font/google (CSS variable) - Remove: vite.config.js, entry-server.jsx, prerender scripts, LanguageContext Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
69
src/components/layout/Header.tsx
Normal file
69
src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { getDictionary, type Locale } from '@/lib/i18n'
|
||||
|
||||
export default function Header({ lang }: { lang: string }) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const d = getDictionary(lang)
|
||||
|
||||
const links = [
|
||||
{ label: d.nav.services, href: `/${lang}/uslugi/` },
|
||||
{ label: d.nav.about, href: `/${lang}/#about` },
|
||||
{ label: d.nav.faq, href: `/${lang}/faq/` },
|
||||
]
|
||||
|
||||
function toggleLang() {
|
||||
const newLang = lang === 'ru' ? 'en' : 'ru'
|
||||
const newPath = pathname.replace(`/${lang}`, `/${newLang}`)
|
||||
router.push(newPath)
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white shadow-sm">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
|
||||
<Link href={`/${lang}/`} className="flex items-center">
|
||||
<Image src="/logo.png" alt="Сисадмингрупп" width={48} height={48} className="h-12 w-auto" />
|
||||
</Link>
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{links.map(l => (
|
||||
<Link key={l.href} href={l.href} className="text-sm font-medium text-slate-600 hover:text-blue-700 transition-colors">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
<button onClick={toggleLang} className="text-sm font-semibold text-slate-500 hover:text-blue-700 transition-colors border border-slate-200 rounded px-2 py-0.5">
|
||||
{lang === 'ru' ? 'EN' : 'RU'}
|
||||
</button>
|
||||
<Link href={`/${lang}/kontakty/`} className="px-4 py-2 bg-blue-700 hover:bg-blue-800 text-white text-sm font-semibold rounded transition-colors">
|
||||
{d.nav.contact}
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="md:hidden flex items-center gap-3">
|
||||
<button onClick={toggleLang} className="text-sm font-semibold text-slate-500 border border-slate-200 rounded px-2 py-0.5">
|
||||
{lang === 'ru' ? 'EN' : 'RU'}
|
||||
</button>
|
||||
<button onClick={() => setMenuOpen(!menuOpen)} className="p-1 text-slate-700">
|
||||
{menuOpen ? <X size={22} /> : <Menu size={22} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{menuOpen && (
|
||||
<div className="md:hidden bg-white border-t border-slate-100 px-4 py-4 flex flex-col gap-4">
|
||||
{links.map(l => (
|
||||
<Link key={l.href} href={l.href} onClick={() => setMenuOpen(false)} className="text-sm font-medium text-slate-700 hover:text-blue-700">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
<Link href={`/${lang}/kontakty/`} onClick={() => setMenuOpen(false)} className="text-sm font-medium text-blue-700">
|
||||
{d.nav.contact}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user