diff --git a/artifacts/ehsan-poc/src/components/OpportunityCard.tsx b/artifacts/ehsan-poc/src/components/OpportunityCard.tsx index 4a9a0dd..1a944ab 100644 --- a/artifacts/ehsan-poc/src/components/OpportunityCard.tsx +++ b/artifacts/ehsan-poc/src/components/OpportunityCard.tsx @@ -4,7 +4,7 @@ import { useLanguage } from "../contexts/LanguageContext"; import { useCart } from "../contexts/CartContext"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Share2, ShoppingCart, Check, Trash2 } from "lucide-react"; +import { Share2, ShoppingCart } from "lucide-react"; import { getNeedImage } from "../lib/needImages"; import { Riyal } from "@/components/Riyal"; @@ -110,20 +110,18 @@ export function OpportunityCard({ request }: OpportunityCardProps) { {/* Donate / cart row */} {inCart ? (
- - + {t.cart.added}
diff --git a/artifacts/ehsan-poc/src/lib/i18n/translations.ts b/artifacts/ehsan-poc/src/lib/i18n/translations.ts index ca69b4c..76f633a 100644 --- a/artifacts/ehsan-poc/src/lib/i18n/translations.ts +++ b/artifacts/ehsan-poc/src/lib/i18n/translations.ts @@ -344,6 +344,9 @@ export const en = { empty: "Your donation cart is empty", emptyHint: "Browse the available opportunities and add the causes you wish to support.", browse: "Browse Opportunities", + amountRequired: "Please enter an amount for each item in the cart.", + itemsCount: "Items", + successMessage: "Thank you. Your donations have been completed successfully. May Allah reward you.", }, admin: { title: "Admin Dashboard", @@ -742,6 +745,9 @@ export const ar = { empty: "سلة تبرعاتك فارغة", emptyHint: "تصفّح الفرص المتاحة وأضف القضايا التي ترغب بدعمها.", browse: "تصفّح الفرص", + amountRequired: "الرجاء إدخال قيمة المبلغ لكل عنصر في السلة.", + itemsCount: "عدد العناصر", + successMessage: "شكراً لك. تمت تبرعاتك بنجاح. جزاك الله خيراً.", }, admin: { title: "لوحة الإدارة", diff --git a/artifacts/ehsan-poc/src/pages/cart.tsx b/artifacts/ehsan-poc/src/pages/cart.tsx index 7b36ba3..48371c8 100644 --- a/artifacts/ehsan-poc/src/pages/cart.tsx +++ b/artifacts/ehsan-poc/src/pages/cart.tsx @@ -1,28 +1,123 @@ +import { useState } from "react"; import { useLocation, Link } from "wouter"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { useLanguage } from "../contexts/LanguageContext"; import { useCart } from "../contexts/CartContext"; +import { + useDonateToRequest, + getListRequestsQueryKey, + getListPublishedRequestsQueryKey, +} from "@workspace/api-client-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ShoppingCart, Trash2, ChevronLeft, ChevronRight } from "lucide-react"; +import { ShoppingCart, Trash2, ChevronLeft, ChevronRight, CheckCircle, Heart } from "lucide-react"; import { getNeedImage } from "../lib/needImages"; import { Riyal } from "@/components/Riyal"; import { Reveal } from "../components/Reveal"; import leafPattern from "@assets/right-snapel-2_1780688632733.svg"; +const schema = z.object({ + donorName: z.string().min(2), + donorPhone: z.string().min(10), + donorEmail: z.string().email().optional().or(z.literal("")), +}); + +type FormData = z.infer; + export default function Cart() { const { t, dir } = useLanguage(); const [, setLocation] = useLocation(); - const { items, removeItem, updateAmount, total } = useCart(); + const queryClient = useQueryClient(); + const { items, removeItem, updateAmount, total, clear } = useCart(); + const donateMutation = useDonateToRequest(); + + const [step, setStep] = useState<"cart" | "payment">("cart"); + const [amountError, setAmountError] = useState(false); + const [submitError, setSubmitError] = useState(false); + const [done, setDone] = useState(false); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { donorName: "", donorPhone: "", donorEmail: "" }, + }); const Chevron = dir === "rtl" ? ChevronLeft : ChevronRight; - const proceed = () => { + const goToPayment = () => { if (items.length === 0) return; - const first = items[0]; - const suffix = first.amount > 0 ? `?amount=${first.amount}` : ""; - setLocation(`/donate/${first.id}${suffix}`); + if (!items.every((i) => i.amount > 0)) { + setAmountError(true); + return; + } + setAmountError(false); + setStep("payment"); }; + const onSubmit = async (data: FormData) => { + // Re-validate amounts at submit time in case the user edited a value + // back to zero after entering the payment step. + if (!items.every((i) => i.amount > 0)) { + setSubmitError(false); + setStep("cart"); + setAmountError(true); + return; + } + setSubmitError(false); + let failed = false; + try { + for (const item of items) { + await donateMutation.mutateAsync({ + id: item.id, + data: { + donorName: data.donorName, + donorPhone: data.donorPhone, + donorEmail: data.donorEmail || null, + amount: item.amount, + }, + }); + removeItem(item.id); + } + } catch { + failed = true; + setSubmitError(true); + } finally { + // Always refresh request data: even on partial failure, the donations + // that did succeed have already changed server state. + queryClient.invalidateQueries({ queryKey: getListRequestsQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListPublishedRequestsQueryKey() }); + } + if (!failed) { + clear(); + setDone(true); + } + }; + + // Success screen after a completed multi-item donation + if (done) { + return ( +
+
+ + +

{t.common.success}

+

{t.cart.successMessage}

+
+ + +
+
+
+ ); + } + return (
{/* Decorative leaf background */} @@ -124,7 +219,10 @@ export default function Cart() { type="number" min={1} value={item.amount > 0 ? item.amount : ""} - onChange={(e) => updateAmount(item.id, Number(e.target.value))} + onChange={(e) => { + updateAmount(item.id, Number(e.target.value)); + setAmountError(false); + }} placeholder={t.cart.amountLabel} className="ps-8" data-testid={`input-amount-${item.id}`} @@ -137,22 +235,110 @@ export default function Cart() { })}
- {/* Summary panel */} + {/* Summary / checkout panel */}
+
+ {t.cart.itemsCount} + + {items.length} + +
{t.cart.total} - + {total.toLocaleString()}
- + + {step === "cart" ? ( + <> + {amountError && ( +

+ {t.cart.amountRequired} +

+ )} + + + ) : ( +
+ +

{t.donate.paymentTitle}

+ ( + + {t.donate.donorName} + + + + + + )} + /> + ( + + {t.donate.donorPhone} + + + + + + )} + /> + ( + + {t.donate.donorEmail} + + + + + + )} + /> + {submitError && ( +

+ {t.common.error} +

+ )} +
+ + +
+ + + )}
diff --git a/attached_assets/image_1780689567898.png b/attached_assets/image_1780689567898.png new file mode 100644 index 0000000..7588557 Binary files /dev/null and b/attached_assets/image_1780689567898.png differ diff --git a/attached_assets/image_1780690235820.png b/attached_assets/image_1780690235820.png new file mode 100644 index 0000000..2fe26b4 Binary files /dev/null and b/attached_assets/image_1780690235820.png differ