EHSAN official look & auth (Task #5)

Reskin the EHSAN POC to match ehsan.sa and gate the admin area behind a
simple POC login.

- Official header: cropped EHSAN logo, nav (الرئيسية/الوقف/فرص التبرع/
  خدماتنا dropdown→طلب دعم/عن إحسان/براعم إحسان), login/cart/search icons,
  language toggle, mobile menu. Functional items link; rest are visual-only.
- Colors: green primary tuned + orange accent token added in index.css.
- Auth: AuthContext (localStorage, admin/admin) + login page; /admin and
  /whatsapp-log now behind a Protected wrapper redirecting to /login.
- Redesigned OpportunityCard (photo, green progress bar with %, category
  badge, تم جمع/المبلغ المتبقي columns, inline donate button + amount),
  used on home and opportunities pages.
- Two-step donate page (التفاصيل → الدفع): step indicator, presets
  100/50/10, custom amount (prefilled via ?amount=), "تبرع عن أهلك"
  checkbox, donor form (phone min 10 for OpenClaw loop).
- 8 need-type card images added; needImages helper maps need→image.
- Seed: added published opportunities (req-012..017) with partial progress
  to showcase cards; one kept near-full for an easy closed-loop demo.

Deviation (from code review): donate handler now ACCUMULATES collectedAmount
and clamps to target, validates finite/positive amount, and only advances to
the closed-loop pipeline when a case is fully funded (previously overwrote
and force-advanced — broke partial-progress cases). Donate buttons kept green
to match the reference; orange is an accent only.
This commit is contained in:
Replit Agent
2026-06-05 17:45:17 +00:00
parent 4db9f09195
commit 5d40b0d3c2
28 changed files with 1177 additions and 324 deletions
@@ -1,5 +1,6 @@
import { ReactNode } from "react";
import { Header } from "./Header";
import ehsanLogo from "../../assets/ehsan-logo.png";
export function AppLayout({ children }: { children: ReactNode }) {
return (
@@ -9,8 +10,11 @@ export function AppLayout({ children }: { children: ReactNode }) {
{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 className="container mx-auto px-4 flex flex-col items-center gap-3 text-center">
<img src={ehsanLogo} alt="EHSAN" className="h-9 w-auto object-contain" />
<p className="text-sm text-muted-foreground">
EHSAN POC &copy; {new Date().getFullYear()}
</p>
</div>
</footer>
</div>
@@ -1,57 +1,216 @@
import { useState } from "react";
import { Link, useLocation } from "wouter";
import { useLanguage } from "../../contexts/LanguageContext";
import { useAuth } from "../../contexts/AuthContext";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import {
Search,
ShoppingCart,
User,
ChevronDown,
Menu,
X,
LogOut,
Globe,
} from "lucide-react";
import ehsanLogo from "../../assets/ehsan-logo.png";
export function Header() {
const { language, setLanguage, t } = useLanguage();
const [location] = useLocation();
const { isAuthenticated, logout } = useAuth();
const [location, setLocation] = useLocation();
const [mobileOpen, setMobileOpen] = useState(false);
const toggleLanguage = () => {
setLanguage(language === "ar" ? "en" : "ar");
};
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 },
];
const isActive = (path: string) => location === path;
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>
<header className="bg-white sticky top-0 z-50 shadow-sm">
<div className="container mx-auto px-4 h-16 flex items-center justify-between gap-4">
{/* Logo */}
<Link href="/" className="flex items-center shrink-0">
<img src={ehsanLogo} alt={t.common.ehsan} className="h-11 w-auto object-contain" />
</Link>
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={toggleLanguage} className="font-medium">
{t.common.language}
{/* Desktop nav */}
<nav className="hidden lg:flex items-center gap-1 flex-1 justify-center">
<Link
href="/"
className={`px-3 py-2 text-sm font-medium rounded-md transition-colors ${
isActive("/") ? "text-primary" : "text-foreground hover:text-primary"
}`}
data-testid="nav-home"
>
{t.nav.home}
</Link>
<span className="px-3 py-2 text-sm font-medium text-foreground/70 cursor-default select-none">
{t.nav.waqf}
</span>
<Link
href="/opportunities"
className="px-4 py-2 text-sm font-bold rounded-md bg-primary text-primary-foreground inline-flex items-center gap-1 hover:opacity-90 transition-opacity"
data-testid="nav-opportunities"
>
{t.nav.opportunities}
<ChevronDown className="w-3.5 h-3.5" />
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="px-3 py-2 text-sm font-medium text-foreground hover:text-primary inline-flex items-center gap-1 transition-colors"
data-testid="nav-services"
>
{t.nav.services}
<ChevronDown className="w-3.5 h-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem onClick={() => setLocation("/request")} data-testid="nav-requestSupport">
{t.nav.requestSupport}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<span className="px-3 py-2 text-sm font-medium text-foreground/70 cursor-default select-none">
{t.nav.about}
</span>
<span className="px-3 py-2 text-sm font-medium text-foreground/70 cursor-default select-none">
{t.nav.baraem}
</span>
</nav>
{/* Utility icons */}
<div className="flex items-center gap-1 shrink-0">
{isAuthenticated ? (
<Button
variant="ghost"
size="sm"
className="gap-1.5 hidden sm:inline-flex"
onClick={() => { logout(); setLocation("/"); }}
data-testid="button-logout"
>
<LogOut className="w-4 h-4" />
<span className="hidden md:inline">{t.common.logout}</span>
</Button>
) : (
<Link
href="/login"
className="hidden sm:inline-flex items-center gap-1.5 px-2.5 py-2 text-sm font-medium text-foreground hover:text-primary transition-colors"
data-testid="button-login"
>
<User className="w-4 h-4" />
<span className="hidden md:inline">{t.common.login}</span>
</Link>
)}
<Button
variant="ghost"
size="icon"
className="text-foreground"
aria-label={t.common.cart}
data-testid="button-cart"
>
<ShoppingCart className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-foreground"
aria-label={t.common.search}
onClick={() => setLocation("/")}
data-testid="button-search"
>
<Search className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={toggleLanguage}
className="gap-1.5 font-medium"
data-testid="button-language"
>
<Globe className="w-4 h-4" />
<span className="hidden md:inline">{t.common.language}</span>
</Button>
{/* Mobile menu toggle */}
<Button
variant="ghost"
size="icon"
className="lg:hidden text-foreground"
onClick={() => setMobileOpen((v) => !v)}
aria-label="Menu"
data-testid="button-mobileMenu"
>
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</Button>
</div>
</div>
{/* Mobile nav panel */}
{mobileOpen && (
<div className="lg:hidden border-t bg-white px-4 py-3 space-y-1">
<Link
href="/"
onClick={() => setMobileOpen(false)}
className="block px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
{t.nav.home}
</Link>
<Link
href="/opportunities"
onClick={() => setMobileOpen(false)}
className="block px-3 py-2 rounded-md text-sm font-bold text-primary hover:bg-muted"
>
{t.nav.opportunities}
</Link>
<Link
href="/request"
onClick={() => setMobileOpen(false)}
className="block px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
{t.nav.requestSupport}
</Link>
<div className="pt-2 mt-2 border-t">
{isAuthenticated ? (
<>
<Link
href="/admin"
onClick={() => setMobileOpen(false)}
className="block px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
{t.nav.admin}
</Link>
<button
onClick={() => { logout(); setMobileOpen(false); setLocation("/"); }}
className="block w-full text-start px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
{t.common.logout}
</button>
</>
) : (
<Link
href="/login"
onClick={() => setMobileOpen(false)}
className="block px-3 py-2 rounded-md text-sm font-medium text-foreground hover:bg-muted"
>
{t.common.login}
</Link>
)}
</div>
</div>
)}
</header>
);
}