Task #19: Donation cart (سلة تبرعاتك) for EHSAN POC

Built a real donation cart matching ehsan.sa:
- CartContext provider (localStorage-persisted, defensive parsing) wired into App.tsx
- OpportunityCard: cart icon now adds the case with its typed amount and swaps the
  action row to an added state («مضاف لسلة تبرعاتك» + «إزالة»)
- Header cart button: live count badge + navigates to /cart
- New /cart page + route: breadcrumb, item list (delete icon, category, name,
  editable «قيمة المبلغ» amount, image+progress%), summary panel «الإجمالي» +
  green «للمتابعة للدفع», decorative leaf SVG background, empty state
- translations.ts: parallel AR+EN `cart` section
- donate.tsx: removes the donated case from the cart on successful donation
  (cart reconciliation), preventing stale added-state/badge

Notes/deviations:
- Checkout handoff routes the first cart item into the existing single-case donate
  flow (the POC backend has no multi-item payment). Reconciliation keeps remaining
  items coherent. A true multi-item checkout backend was out of scope.
- Verified with passing e2e test (add → badge → cart page → remove → empty) and
  clean tsc; architect review addressed (reconciliation + defensive parsing).
This commit is contained in:
Replit Agent
2026-06-05 19:54:45 +00:00
parent b37e5fdfdb
commit d0d504bc74
8 changed files with 388 additions and 42 deletions
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { Link, useLocation } from "wouter";
import { useLanguage } from "../../contexts/LanguageContext";
import { useAuth } from "../../contexts/AuthContext";
import { useCart } from "../../contexts/CartContext";
import { Button } from "../ui/button";
import {
Search,
@@ -29,6 +30,7 @@ import ehsanLogo from "../../assets/ehsan-logo.png";
export function Header() {
const { language, setLanguage, t } = useLanguage();
const { isAuthenticated, logout } = useAuth();
const { count: cartCount } = useCart();
const [location, setLocation] = useLocation();
const [mobileOpen, setMobileOpen] = useState(false);
const [servicesOpen, setServicesOpen] = useState(false);
@@ -218,11 +220,20 @@ export function Header() {
<Button
variant="ghost"
size="icon"
className="text-foreground"
className="text-foreground relative"
aria-label={t.common.cart}
onClick={() => setLocation("/cart")}
data-testid="button-cart"
>
<ShoppingCart className="w-4 h-4" />
{cartCount > 0 && (
<span
className="absolute -top-1 -end-1 min-w-[1.1rem] h-[1.1rem] px-1 rounded-full bg-primary text-primary-foreground text-[0.65rem] font-bold flex items-center justify-center leading-none"
data-testid="badge-cart-count"
>
{cartCount}
</span>
)}
</Button>
<Button
variant="ghost"