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
+2
View File
@@ -10,6 +10,7 @@ import { ComponentType } from "react";
// Page imports
import Home from "./pages/home";
import About from "./pages/about";
import RequestSupport from "./pages/request";
import Opportunities from "./pages/opportunities";
import Donate from "./pages/donate";
@@ -34,6 +35,7 @@ function Router() {
<AppLayout>
<Switch>
<Route path="/" component={Home} />
<Route path="/about/:section?" component={About} />
<Route path="/request" component={RequestSupport} />
<Route path="/opportunities" component={Opportunities} />
<Route path="/donate/:id" component={Donate} />
@@ -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 ? (
<>
@@ -43,6 +43,73 @@ export const en = {
whatsappLog: "WhatsApp Log",
quickDonate: "Quick Donate",
},
about: {
pageTitle: "About EHSAN",
whoWeAre: "Who We Are",
committees: "Committees",
introTitle: "About EHSAN",
intro:
"EHSAN was established by Royal Order No. (48019) dated 13/08/1441 AH to harness data and artificial intelligence to maximize the impact and sustainability of developmental projects and services, by delivering advanced technical solutions and building an effective ecosystem through partnerships with the governmental, private, and non-profit sectors, with the aim of strengthening the Kingdom of Saudi Arabia's leading role in developmental and charitable work, and raising the contribution of the non-profit sector to the gross domestic product.",
tabVisionMission: "Vision & Mission",
tabGoalsPillars: "Goals & Pillars",
tabAdvantagesDomains: "Advantages & Domains",
visionTitle: "Our Vision",
visionText:
"To be the trusted national platform that empowers charitable and developmental work and enhances its sustainability, contributing to a higher quality of life.",
missionTitle: "Our Mission",
missionText:
"To provide a trusted national platform that facilitates giving and connects donors with those in need transparently and efficiently through advanced technical solutions.",
goalsTitle: "Goals",
goals: [
"Strengthen trust in charitable work through transparency and governance.",
"Facilitate the efficient delivery of donations to their beneficiaries.",
"Raise the contribution of the non-profit sector to the gross domestic product.",
"Empower charitable entities with advanced technical solutions.",
],
pillarsTitle: "Pillars",
pillars: [
"Transparency and reliability.",
"Technical innovation and artificial intelligence.",
"Effective partnerships.",
"Sustainability.",
],
advantagesTitle: "Advantages",
advantages: [
"A unified, accredited national donation platform.",
"Safe and trusted donations.",
"A wide variety of giving options.",
"Tracking the impact of your donation.",
],
domainsTitle: "Domains",
domains: [
"Humanitarian and social aid.",
"Health.",
"Education.",
"Housing.",
"Endowments.",
],
committeesTitle: "Committees",
committeesIntro:
"EHSAN's work is overseen by a set of specialized committees that ensure governance, transparency, and Sharia compliance.",
committeeItems: [
{
name: "Supervisory Committee",
desc: "Oversees the platform's operations and strategic direction.",
},
{
name: "Executive Committee",
desc: "Follows up on the execution of operational plans and initiatives.",
},
{
name: "Audit & Governance Committee",
desc: "Ensures commitment to governance and transparency.",
},
{
name: "Sharia Committee",
desc: "Verifies that operations comply with Islamic rulings.",
},
],
},
serviceItems: {
ghiras: "Ghiras",
zakat: "Zakat",
@@ -265,6 +332,73 @@ export const ar = {
whatsappLog: "سجل واتساب",
quickDonate: "تبرع سريع",
},
about: {
pageTitle: "عن إحسان",
whoWeAre: "من نحن",
committees: "اللجان",
introTitle: "نبذة عن إحسان",
intro:
"جاءت منصة إحسان الصادرة بالأمر السامي رقم (48019) وتاريخه (1441/08/13هـ) لتعمل على استثمار البيانات والذكاء الاصطناعي لتعظيم أثر المشاريع والخدمات التنموية واستدامتها، من خلال تقديم الحلول التقنية المتقدمة وبناء منظومة فاعلة عبر الشراكات مع القطاعات الحكومية والخاصة وغير الربحية، بهدف تعزيز دور المملكة العربية السعودية الريادي في الأعمال التنموية والخيرية، ورفع مساهمة القطاع غير الربحي في إجمالي الناتج المحلي.",
tabVisionMission: "الرؤية والرسالة",
tabGoalsPillars: "الأهداف والركائز",
tabAdvantagesDomains: "المزايا والمجالات",
visionTitle: "رؤيتنا",
visionText:
"أن تكون إحسان المنصة الوطنية الموثوقة التي تمكّن العمل الخيري والتنموي وتعزز استدامته بما يسهم في رفع جودة الحياة.",
missionTitle: "رسالتنا",
missionText:
"توفير منصة وطنية موثوقة تسهّل التبرع وتربط المتبرعين بالمحتاجين بشفافية وكفاءة عبر حلول تقنية متقدمة.",
goalsTitle: "الأهداف",
goals: [
"تعزيز الثقة في العمل الخيري عبر الشفافية والحوكمة.",
"تسهيل وصول التبرعات إلى مستحقيها بكفاءة.",
"رفع مساهمة القطاع غير الربحي في إجمالي الناتج المحلي.",
"تمكين الجهات الخيرية بالحلول التقنية المتقدمة.",
],
pillarsTitle: "الركائز",
pillars: [
"الشفافية والموثوقية.",
"الابتكار التقني والذكاء الاصطناعي.",
"الشراكات الفاعلة.",
"الاستدامة.",
],
advantagesTitle: "المزايا",
advantages: [
"منصة وطنية موحدة ومعتمدة للتبرع.",
"تبرع آمن وموثوق.",
"تنوع في أوجه التبرع.",
"متابعة أثر التبرع.",
],
domainsTitle: "المجالات",
domains: [
"المساعدات الإنسانية والاجتماعية.",
"الصحة.",
"التعليم.",
"الإسكان.",
"الأوقاف.",
],
committeesTitle: "اللجان",
committeesIntro:
"يشرف على أعمال منصة إحسان مجموعة من اللجان المتخصصة التي تضمن الحوكمة والشفافية والتوافق الشرعي.",
committeeItems: [
{
name: "اللجنة الإشرافية",
desc: "الإشراف على أعمال المنصة وتوجهاتها الاستراتيجية.",
},
{
name: "اللجنة التنفيذية",
desc: "متابعة تنفيذ الخطط والمبادرات التشغيلية.",
},
{
name: "لجنة المراجعة والحوكمة",
desc: "ضمان الالتزام بالحوكمة والشفافية.",
},
{
name: "اللجنة الشرعية",
desc: "التأكد من توافق الأعمال مع الأحكام الشرعية.",
},
],
},
serviceItems: {
ghiras: "غراس",
zakat: "الزكاة",
+165
View File
@@ -0,0 +1,165 @@
import { useState } from "react";
import { useParams, Link } from "wouter";
import { useLanguage } from "../contexts/LanguageContext";
import { Reveal } from "../components/Reveal";
import { Card, CardContent } from "@/components/ui/card";
import { Users, Info, Check, Building2 } from "lucide-react";
type TabKey = "visionMission" | "goalsPillars" | "advantagesDomains";
function ListBlock({ title, items }: { title: string; items: string[] }) {
return (
<div>
<h3 className="text-lg font-bold text-primary mb-4">{title}</h3>
<ul className="space-y-3">
{items.map((item, i) => (
<li key={i} className="flex items-start gap-3">
<Check className="w-5 h-5 text-primary shrink-0 mt-0.5" />
<span className="text-foreground/90 leading-relaxed">{item}</span>
</li>
))}
</ul>
</div>
);
}
function StatementBlock({ title, text }: { title: string; text: string }) {
return (
<div>
<h3 className="text-lg font-bold text-primary mb-3">{title}</h3>
<p className="text-foreground/90 leading-loose">{text}</p>
</div>
);
}
export default function About() {
const { t } = useLanguage();
const params = useParams<{ section?: string }>();
const isCommittees = params.section === "committees";
const [tab, setTab] = useState<TabKey>("visionMission");
const tabs: { key: TabKey; label: string }[] = [
{ key: "visionMission", label: t.about.tabVisionMission },
{ key: "goalsPillars", label: t.about.tabGoalsPillars },
{ key: "advantagesDomains", label: t.about.tabAdvantagesDomains },
];
return (
<div className="container mx-auto px-4 py-10">
{/* Section toggle: Who we are / Committees */}
<div className="flex justify-end mb-8">
<div className="inline-flex rounded-full border border-border bg-muted/40 p-1">
<Link
href="/about"
className={`inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-medium transition-colors ${
!isCommittees
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-primary"
}`}
data-testid="about-tab-whoWeAre"
>
<Info className="w-4 h-4" />
{t.about.whoWeAre}
</Link>
<Link
href="/about/committees"
className={`inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-medium transition-colors ${
isCommittees
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-primary"
}`}
data-testid="about-tab-committees"
>
<Users className="w-4 h-4" />
{t.about.committees}
</Link>
</div>
</div>
{isCommittees ? (
<Reveal>
<h1 className="text-2xl font-bold text-foreground mb-3">{t.about.committeesTitle}</h1>
<p className="text-muted-foreground mb-8 max-w-3xl leading-relaxed">
{t.about.committeesIntro}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{t.about.committeeItems.map((c, i) => (
<Reveal key={i} delay={(i % 2) * 0.08} className="h-full">
<Card className="h-full">
<CardContent className="p-6 flex items-start gap-4">
<div className="w-11 h-11 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0">
<Building2 className="w-5 h-5" />
</div>
<div>
<h3 className="text-base font-bold text-foreground mb-1">{c.name}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{c.desc}</p>
</div>
</CardContent>
</Card>
</Reveal>
))}
</div>
</Reveal>
) : (
<>
{/* Intro green panel */}
<Reveal>
<section className="rounded-2xl bg-primary text-primary-foreground p-8 md:p-12 mb-10">
<h1 className="text-2xl md:text-3xl font-bold mb-5">{t.about.introTitle}</h1>
<p className="leading-loose text-primary-foreground/90 max-w-4xl">
{t.about.intro}
</p>
</section>
</Reveal>
{/* Tabs */}
<Reveal>
<div className="border-b border-border mb-8">
<div className="flex flex-wrap gap-2">
{tabs.map((tb) => (
<button
key={tb.key}
type="button"
onClick={() => setTab(tb.key)}
data-testid={`about-section-${tb.key}`}
className={`px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors ${
tab === tb.key
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary"
}`}
>
{tb.label}
</button>
))}
</div>
</div>
</Reveal>
{/* Tab content */}
<Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12">
{tab === "visionMission" && (
<>
<StatementBlock title={t.about.visionTitle} text={t.about.visionText} />
<StatementBlock title={t.about.missionTitle} text={t.about.missionText} />
</>
)}
{tab === "goalsPillars" && (
<>
<ListBlock title={t.about.goalsTitle} items={t.about.goals} />
<ListBlock title={t.about.pillarsTitle} items={t.about.pillars} />
</>
)}
{tab === "advantagesDomains" && (
<>
<ListBlock title={t.about.advantagesTitle} items={t.about.advantages} />
<ListBlock title={t.about.domainsTitle} items={t.about.domains} />
</>
)}
</div>
</Reveal>
</>
)}
</div>
);
}