first udpate
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { CommonContent } from "@/content/types";
|
||||
import { locales, type Locale } from "@/lib/i18n";
|
||||
|
||||
type BottomNavProps = {
|
||||
locale: Locale;
|
||||
common: CommonContent;
|
||||
};
|
||||
|
||||
export default function BottomNav({ locale, common }: BottomNavProps) {
|
||||
const pathname = usePathname();
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
|
||||
const normalizedPath = pathname !== "/" && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||
const homePath = `/${locale}`;
|
||||
const aboutPath = `/${locale}/about`;
|
||||
const contactPath = `/${locale}/contact`;
|
||||
|
||||
const isHome = normalizedPath === homePath;
|
||||
const isAbout = normalizedPath === aboutPath || normalizedPath.startsWith(`${aboutPath}/`);
|
||||
const isContact = normalizedPath === contactPath || normalizedPath.startsWith(`${contactPath}/`);
|
||||
const targetLocale: Locale = locale === "ar" ? "en" : "ar";
|
||||
|
||||
const switchPath = () => {
|
||||
const nextSegments = [...segments];
|
||||
|
||||
if (nextSegments.length === 0) {
|
||||
return `/${targetLocale}`;
|
||||
}
|
||||
|
||||
if (locales.includes(nextSegments[0] as Locale)) {
|
||||
nextSegments[0] = targetLocale;
|
||||
} else {
|
||||
nextSegments.unshift(targetLocale);
|
||||
}
|
||||
|
||||
return `/${nextSegments.join("/")}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<nav aria-label={common.navLabel} className="bottom-nav">
|
||||
<div className="container bottom-nav-inner">
|
||||
<div className="bottom-nav-links">
|
||||
<Link href={homePath} className={isHome ? "active" : undefined} aria-current={isHome ? "page" : undefined}>
|
||||
<span aria-hidden className="nav-icon">
|
||||
○
|
||||
</span>
|
||||
{common.nav.home}
|
||||
</Link>
|
||||
<Link href={aboutPath} className={isAbout ? "active" : undefined} aria-current={isAbout ? "page" : undefined}>
|
||||
<span aria-hidden className="nav-icon">
|
||||
□
|
||||
</span>
|
||||
{common.nav.about}
|
||||
</Link>
|
||||
<Link
|
||||
href={contactPath}
|
||||
className={isContact ? "active" : undefined}
|
||||
aria-current={isContact ? "page" : undefined}
|
||||
>
|
||||
<span aria-hidden className="nav-icon">
|
||||
◇
|
||||
</span>
|
||||
{common.nav.contact}
|
||||
</Link>
|
||||
<Link href={switchPath()} className="lang-toggle" aria-label={common.languageSwitcherLabel}>
|
||||
<span aria-hidden className="nav-icon">
|
||||
⇄
|
||||
</span>
|
||||
{targetLocale.toUpperCase()}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Link from "next/link";
|
||||
import type { CommonContent, HomeContent } from "@/content/types";
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
|
||||
type HeroSectionProps = {
|
||||
locale: Locale;
|
||||
home: HomeContent;
|
||||
common: CommonContent;
|
||||
};
|
||||
|
||||
export default function HeroSection({ locale, home, common }: HeroSectionProps) {
|
||||
return (
|
||||
<section className="hero panel">
|
||||
<p className="eyebrow">{home.kicker}</p>
|
||||
<h1>{home.title}</h1>
|
||||
<p className="lead">{home.description}</p>
|
||||
|
||||
<div className="cta-row">
|
||||
<Link href={`/${locale}/contact`} className="cta-btn">
|
||||
{home.primaryCta}
|
||||
</Link>
|
||||
<Link href={`/${locale}/about`} className="ghost-btn">
|
||||
{common.nav.about}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="stat-grid">
|
||||
{home.highlights.map((item) => (
|
||||
<article key={item.label} className="stat-card">
|
||||
<span className="stat-value">{item.value}</span>
|
||||
<span className="stat-label">{item.label}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { locales, type Locale } from "@/lib/i18n";
|
||||
|
||||
type LanguageSwitcherProps = {
|
||||
locale: Locale;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default function LanguageSwitcher({ locale, label }: LanguageSwitcherProps) {
|
||||
const pathname = usePathname();
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
|
||||
const switchPath = (targetLocale: Locale) => {
|
||||
const nextSegments = [...segments];
|
||||
|
||||
if (nextSegments.length === 0) {
|
||||
return `/${targetLocale}`;
|
||||
}
|
||||
|
||||
if (locales.includes(nextSegments[0] as Locale)) {
|
||||
nextSegments[0] = targetLocale;
|
||||
} else {
|
||||
nextSegments.unshift(targetLocale);
|
||||
}
|
||||
|
||||
return `/${nextSegments.join("/")}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="language-switcher" aria-label={label}>
|
||||
{locales.map((item) => {
|
||||
const active = item === locale;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item}
|
||||
href={switchPath(item)}
|
||||
className={active ? "lang-chip active" : "lang-chip"}
|
||||
aria-current={active ? "page" : undefined}
|
||||
>
|
||||
{item.toUpperCase()}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
import type { CommonContent } from "@/content/types";
|
||||
|
||||
type SiteFooterProps = {
|
||||
locale: Locale;
|
||||
common: CommonContent;
|
||||
};
|
||||
|
||||
export default function SiteFooter({ locale, common }: SiteFooterProps) {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="container footer-content">
|
||||
<p>{common.footerRights.replace("{year}", String(year))}</p>
|
||||
<p className="locale-badge">{locale.toUpperCase()}</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Link from "next/link";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
import type { CommonContent } from "@/content/types";
|
||||
|
||||
type SiteHeaderProps = {
|
||||
locale: Locale;
|
||||
common: CommonContent;
|
||||
};
|
||||
|
||||
export default function SiteHeader({ locale, common }: SiteHeaderProps) {
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="container bar">
|
||||
<Link href={`/${locale}`} className="brand">
|
||||
{common.siteTitle}
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
const STORAGE_KEY = "theme";
|
||||
|
||||
function isTheme(value: string | null): value is Theme {
|
||||
return value === "dark" || value === "light";
|
||||
}
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<Theme>("dark");
|
||||
|
||||
useEffect(() => {
|
||||
const storedTheme = localStorage.getItem(STORAGE_KEY);
|
||||
const activeTheme: Theme = isTheme(storedTheme) ? storedTheme : "dark";
|
||||
|
||||
document.documentElement.setAttribute("data-theme", activeTheme);
|
||||
setTheme(activeTheme);
|
||||
}, []);
|
||||
|
||||
const handleToggle = () => {
|
||||
const nextTheme: Theme = theme === "dark" ? "light" : "dark";
|
||||
|
||||
setTheme(nextTheme);
|
||||
document.documentElement.setAttribute("data-theme", nextTheme);
|
||||
localStorage.setItem(STORAGE_KEY, nextTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
className="theme-toggle"
|
||||
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
||||
>
|
||||
<span className="theme-toggle-label">{theme === "dark" ? "Light" : "Dark"}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user