diff --git a/.agents/memory/api-server-data.md b/.agents/memory/api-server-data.md index f6a9bda..051a9e6 100644 --- a/.agents/memory/api-server-data.md +++ b/.agents/memory/api-server-data.md @@ -12,3 +12,12 @@ re-seed clean demo data. **How to apply:** after running curl-based API tests that mutate state, restart the api-server workflow before screenshots/handoff so the user sees a clean seeded demo. + +## Donate e2e: use OPEN cases only +Seed cases req-001..req-006 are already in later pipeline stages (fully funded, +remaining 0). The donate page clamps any donation to the case's remaining target, so +on those cases the amount silently becomes 0 and the donate POST returns 400 — you +never reach the success screen. For donation/success-screen e2e, use a `published` +case with remaining > 0 (e.g. req-007, req-012..req-017). +**Why:** cost 3 failed test runs chasing a non-bug. The clamp + funded-seed +interaction is not obvious from the UI alone. diff --git a/artifacts/ehsan-poc/public/opengraph.jpg b/artifacts/ehsan-poc/public/opengraph.jpg index 4eeb675..33452d1 100644 Binary files a/artifacts/ehsan-poc/public/opengraph.jpg and b/artifacts/ehsan-poc/public/opengraph.jpg differ diff --git a/artifacts/ehsan-poc/src/lib/i18n/translations.ts b/artifacts/ehsan-poc/src/lib/i18n/translations.ts index 152ba63..5a14e72 100644 --- a/artifacts/ehsan-poc/src/lib/i18n/translations.ts +++ b/artifacts/ehsan-poc/src/lib/i18n/translations.ts @@ -357,6 +357,12 @@ export const en = { backToDetails: "Back to Details", paymentTitle: "Payment Details", selectAmountError: "Please select or enter a valid amount.", + successTitle: "Thank you for your generous donation", + successSubtitle: "Your donation has been completed successfully!", + receiptNumber: "Receipt Number", + referenceNumber: "Transaction Reference Number", + refundNote: "To make refunds easy, please keep the transaction reference number.", + copied: "Copied", }, cart: { title: "Your Donation Cart", @@ -806,6 +812,12 @@ export const ar = { backToDetails: "رجوع للتفاصيل", paymentTitle: "بيانات الدفع", selectAmountError: "الرجاء اختيار أو إدخال مبلغ صحيح.", + successTitle: "شكرا على تبرعك الكريم", + successSubtitle: "لقد تم إتمام عملية تبرعك بنجاح!", + receiptNumber: "رقم الإيصال", + referenceNumber: "الرقم المرجعي للعملية", + refundNote: "لتتم عملية الإسترداد بسهولة، نأمل حفظ الرقم المرجعي للعملية", + copied: "تم النسخ", }, cart: { title: "سلة تبرعاتك", diff --git a/artifacts/ehsan-poc/src/pages/donate.tsx b/artifacts/ehsan-poc/src/pages/donate.tsx index a9ee4a8..3da8d6a 100644 --- a/artifacts/ehsan-poc/src/pages/donate.tsx +++ b/artifacts/ehsan-poc/src/pages/donate.tsx @@ -15,13 +15,38 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; -import { CheckCircle, Heart, Gift, Check } from "lucide-react"; +import { Gift, Check, Copy, Info } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; import { getNeedImage } from "../lib/needImages"; import { Riyal } from "@/components/Riyal"; const PRESETS = [100, 50, 10]; +// POC: receipt/reference numbers are not returned by the API, so we synthesize +// plausible values on the client at the moment the donation succeeds. +function generateReceiptNo(): string { + let s = ""; + for (let i = 0; i < 15; i++) s += Math.floor(Math.random() * 10); + return s; +} + +function generateReferenceNo(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +// Subtle EHSAN-style overlapping-circles geometric pattern. +const PATTERN_SVG = encodeURIComponent( + `` +); +const PATTERN_BG = `url("data:image/svg+xml,${PATTERN_SVG}")`; + const schema = z.object({ donorName: z.string().min(2), donorPhone: z.string().min(10), @@ -49,6 +74,31 @@ export default function Donate() { const [onBehalf, setOnBehalf] = useState(false); const [onBehalfName, setOnBehalfName] = useState(""); const [donated, setDonated] = useState(false); + const [donatedAmount, setDonatedAmount] = useState(0); + const [receiptNo, setReceiptNo] = useState(""); + const [referenceNo, setReferenceNo] = useState(""); + const [copiedField, setCopiedField] = useState<"receipt" | "reference" | null>(null); + + const copyToClipboard = async (value: string, field: "receipt" | "reference") => { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + } else { + const ta = document.createElement("textarea"); + ta.value = value; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } + setCopiedField(field); + setTimeout(() => setCopiedField((c) => (c === field ? null : c)), 1500); + } catch { + // Clipboard unavailable or permission denied; silently ignore. + } + }; const { data: request, isLoading } = useGetRequest(params.id || "", { query: { enabled: !!params.id, queryKey: getGetRequestQueryKey(params.id || "") }, @@ -88,26 +138,85 @@ export default function Donate() { if (donated) { return ( -
{t.donate.successMessage}
-- {request.caseId} -
-{t.donate.successSubtitle}
+ + {/* Amount */} +