Add About EHSAN dropdown nav + About page

Task #15: Make «عن إحسان» top-nav a real dropdown and build the About page.

- Header.tsx: converted static «عن إحسان» span into a dropdown trigger
  replicating the existing services dropdown pattern (aboutOpen state,
  trigger/panel refs, outside-click + Escape handling). Desktop panel and
  mobile nav both list «من نحن» (/about) and «اللجان» (/about/committees).
- New pages/about.tsx: green intro panel («نبذة عن إحسان») + tab strip
  (الرؤية والرسالة / الأهداف والركائز / المزايا والمجالات) for the Who-we-are
  view, and a committee card grid for the Committees view. Section chosen via
  /about/:section? route param; in-page toggle between the two. Reuses Reveal
  for scroll-in.
- App.tsx: registered <Route path="/about/:section?" component={About} />.
- translations.ts: added full bilingual `about` section (AR + EN).

Fully bilingual via existing t.* system, RTL verified, no emojis.
tsc passes; screenshots confirm both views render correctly.
This commit is contained in:
Replit Agent
2026-06-05 19:30:47 +00:00
parent 887a17cbef
commit 45a52b177f
4 changed files with 408 additions and 2 deletions
@@ -21,6 +21,8 @@ import {
MessageSquare,
TrendingUp,
HeartHandshake,
Info,
Users,
} from "lucide-react";
import ehsanLogo from "../../assets/ehsan-logo.png";
@@ -30,8 +32,11 @@ export function Header() {
const [location, setLocation] = useLocation();
const [mobileOpen, setMobileOpen] = useState(false);
const [servicesOpen, setServicesOpen] = useState(false);
const [aboutOpen, setAboutOpen] = useState(false);
const servicesTriggerRef = useRef<HTMLButtonElement>(null);
const servicesPanelRef = useRef<HTMLDivElement>(null);
const aboutTriggerRef = useRef<HTMLButtonElement>(null);
const aboutPanelRef = useRef<HTMLDivElement>(null);
const toggleLanguage = () => setLanguage(language === "ar" ? "en" : "ar");
@@ -42,6 +47,11 @@ export function Header() {
setLocation(path);
};
const handleAboutClick = (path: string) => {
setAboutOpen(false);
setLocation(path);
};
useEffect(() => {
if (!servicesOpen) return;
const onPointerDown = (e: MouseEvent) => {
@@ -65,6 +75,29 @@ export function Header() {
};
}, [servicesOpen]);
useEffect(() => {
if (!aboutOpen) return;
const onPointerDown = (e: MouseEvent) => {
const target = e.target as Node;
if (
aboutPanelRef.current?.contains(target) ||
aboutTriggerRef.current?.contains(target)
) {
return;
}
setAboutOpen(false);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setAboutOpen(false);
};
document.addEventListener("mousedown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("mousedown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, [aboutOpen]);
const services = [
{ key: "ghiras", label: t.serviceItems.ghiras, icon: Sprout },
{ key: "zakat", label: t.serviceItems.zakat, icon: HandCoins },
@@ -133,9 +166,25 @@ export function Header() {
/>
</button>
<span className="px-3 py-2 text-sm font-medium text-foreground/70 cursor-default select-none">
<button
ref={aboutTriggerRef}
type="button"
onClick={() => setAboutOpen((v) => !v)}
className={`px-3 py-2 text-sm font-medium inline-flex items-center gap-1 transition-colors ${
aboutOpen || location.startsWith("/about")
? "text-primary"
: "text-foreground hover:text-primary"
}`}
data-testid="nav-about"
aria-haspopup="menu"
aria-controls="about-menu"
aria-expanded={aboutOpen}
>
{t.nav.about}
</span>
<ChevronDown
className={`w-3.5 h-3.5 transition-transform ${aboutOpen ? "rotate-180" : ""}`}
/>
</button>
<span className="px-3 py-2 text-sm font-medium text-foreground/70 cursor-default select-none">
{t.nav.baraem}
@@ -250,6 +299,41 @@ export function Header() {
</div>
)}
{/* About dropdown (desktop) */}
{aboutOpen && (
<div
ref={aboutPanelRef}
id="about-menu"
role="menu"
className="hidden lg:block absolute inset-x-0 top-full border-t bg-white shadow-lg"
data-testid="about-menu"
onMouseLeave={() => setAboutOpen(false)}
>
<div className="container mx-auto px-4 py-4">
<div className="flex items-stretch gap-2 max-w-md">
<button
type="button"
onClick={() => handleAboutClick("/about")}
data-testid="nav-about-whoWeAre"
className="flex-1 flex flex-col items-center justify-center gap-2 px-2 py-3 rounded-md text-center hover:bg-muted transition-colors"
>
<Info className="w-7 h-7 text-primary" />
<span className="text-sm font-medium text-foreground">{t.about.whoWeAre}</span>
</button>
<button
type="button"
onClick={() => handleAboutClick("/about/committees")}
data-testid="nav-about-committees"
className="flex-1 flex flex-col items-center justify-center gap-2 px-2 py-3 rounded-md text-center hover:bg-muted transition-colors border-s border-border"
>
<Users className="w-7 h-7 text-primary" />
<span className="text-sm font-medium text-foreground">{t.about.committees}</span>
</button>
</div>
</div>
</div>
)}
{/* Mobile nav panel */}
{mobileOpen && (
<div className="lg:hidden border-t bg-white px-4 py-3 space-y-1">
@@ -299,6 +383,27 @@ export function Header() {
>
{t.nav.requestSupport}
</Link>
<div className="pt-1">
<p className="px-3 py-1 text-xs font-semibold text-foreground/50">
{t.nav.about}
</p>
<Link
href="/about"
onClick={() => setMobileOpen(false)}
className="flex w-full items-center gap-2 text-start px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
<Info className="w-4 h-4 text-primary" />
<span>{t.about.whoWeAre}</span>
</Link>
<Link
href="/about/committees"
onClick={() => setMobileOpen(false)}
className="flex w-full items-center gap-2 text-start px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
<Users className="w-4 h-4 text-primary" />
<span>{t.about.committees}</span>
</Link>
</div>
<div className="pt-2 mt-2 border-t">
{isAuthenticated ? (
<>