first udpate

This commit is contained in:
diyaa
2026-03-13 03:16:25 +01:00
commit 243a182d33
28 changed files with 1144 additions and 0 deletions
+79
View File
@@ -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>
);
}
+37
View File
@@ -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>
);
}
+50
View File
@@ -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>
);
}
+20
View File
@@ -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>
);
}
+22
View File
@@ -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>
);
}
+42
View File
@@ -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>
);
}