Build EHSAN Closed Donation Loop POC — full bilingual Arabic/English app

- Backend (api-server): Complete in-memory mock DB with 11 seed cases, 5 eligible
  beneficiaries, 3 donors, and WhatsApp log. All 14 API routes implemented across
  requests, donors, stats, and whatsapp-log. OpenClaw integration with OPENCLAW_SIMULATE
  toggle. UUID-based IDs. Full status machine (new → closed, 10 steps).

- Frontend (ehsan-poc): 8 pages fully implemented using all generated API hooks:
  Home (stats counters, 10-step workflow diagram), Request (form with eligibility
  result), Opportunities (card grid with progress bars), Donate (case summary +
  donor form), Admin (full data table with contextual action buttons), Track
  (10-step visual timeline in green), ThankYou (message form), WhatsApp Log
  (WhatsApp bubble preview + OpenClaw send button).

- Bilingual LanguageContext (AR/EN) with RTL/LTR toggle, localStorage persistence.
  EHSAN green palette (HSL 143), Tajawal font, fully responsive.
  TypeScript clean — zero errors.
This commit is contained in:
Replit Agent
2026-06-05 17:05:27 +00:00
parent 2da838bb66
commit 12111a9562
117 changed files with 12366 additions and 81 deletions
@@ -0,0 +1,18 @@
import { ReactNode } from "react";
import { Header } from "./Header";
export function AppLayout({ children }: { children: ReactNode }) {
return (
<div className="min-h-[100dvh] flex flex-col bg-background font-sans">
<Header />
<main className="flex-1">
{children}
</main>
<footer className="border-t bg-white mt-auto py-8">
<div className="container mx-auto px-4 text-center text-sm text-muted-foreground">
EHSAN POC &copy; {new Date().getFullYear()}
</div>
</footer>
</div>
);
}
@@ -0,0 +1,57 @@
import { Link, useLocation } from "wouter";
import { useLanguage } from "../../contexts/LanguageContext";
import { Button } from "../ui/button";
export function Header() {
const { language, setLanguage, t } = useLanguage();
const [location] = useLocation();
const toggleLanguage = () => {
setLanguage(language === "ar" ? "en" : "ar");
};
const navItems = [
{ path: "/", label: t.common.home },
{ path: "/opportunities", label: t.common.opportunities },
{ path: "/request", label: t.common.requestSupport },
{ path: "/admin", label: t.common.adminDashboard },
{ path: "/whatsapp-log", label: t.common.whatsappLog },
];
return (
<header className="border-b bg-white sticky top-0 z-50">
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-8">
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-primary flex items-center justify-center text-primary-foreground font-bold">
إ
</div>
<span className="text-xl font-bold text-primary">{t.common.ehsan}</span>
</Link>
<nav className="hidden md:flex items-center gap-1">
{navItems.map((item) => (
<Link
key={item.path}
href={item.path}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
location === item.path
? "text-primary bg-primary/10"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
>
{item.label}
</Link>
))}
</nav>
</div>
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={toggleLanguage} className="font-medium">
{t.common.language}
</Button>
</div>
</div>
</header>
);
}