From 1851c3858b44fd371b6cf0221e4b97457cf3ec63 Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Fri, 5 Jun 2026 20:14:23 +0000 Subject: [PATCH] Add ability for users to donate to multiple items in their cart at once Implement multi-item donation checkout flow, including form validation, error handling for partial failures, and success messaging. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1fa9329f-0cec-4a2f-80e8-e26dbae3142e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2339a9db-a182-40b6-a165-d906737c84f7 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/4d696b13-86f2-4c9d-be0d-95b293430047/1fa9329f-0cec-4a2f-80e8-e26dbae3142e/ZkqO90V Replit-Helium-Checkpoint-Created: true --- .../src/components/OpportunityCard.tsx | 10 +- .../ehsan-poc/src/lib/i18n/translations.ts | 6 + artifacts/ehsan-poc/src/pages/cart.tsx | 218 ++++++++++++++++-- attached_assets/image_1780689567898.png | Bin 0 -> 5540 bytes attached_assets/image_1780690235820.png | Bin 0 -> 2914 bytes 5 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 attached_assets/image_1780689567898.png create mode 100644 attached_assets/image_1780690235820.png 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 0000000000000000000000000000000000000000..7588557e9d44c267768b3d7e66f81fe28e3a49b1 GIT binary patch literal 5540 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 literal 0 HcmV?d00001