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:
@@ -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 © {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 © {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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user