Update donation success screen with translations and improved functionality
Add Arabic and English translations for the donation success screen, including receipt and reference numbers. Implement client-side generation of these numbers with copy-to-clipboard functionality. Update memory data with testing notes regarding donation cases. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1fa9329f-0cec-4a2f-80e8-e26dbae3142e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: a89849bc-f826-44f3-8055-c4618b5fd918 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/4d696b13-86f2-4c9d-be0d-95b293430047/1fa9329f-0cec-4a2f-80e8-e26dbae3142e/4KPAtBh Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
@@ -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: "سلة تبرعاتك",
|
||||
|
||||
@@ -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(
|
||||
`<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'><g fill='none' stroke='#17a85a' stroke-width='1'><circle cx='0' cy='0' r='60'/><circle cx='120' cy='0' r='60'/><circle cx='0' cy='120' r='60'/><circle cx='120' cy='120' r='60'/><circle cx='60' cy='60' r='60'/></g></svg>`
|
||||
);
|
||||
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,17 +138,77 @@ export default function Donate() {
|
||||
|
||||
if (donated) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12 max-w-2xl">
|
||||
<Card className="border-2 border-primary/20">
|
||||
<CardContent className="pt-10 pb-10 text-center">
|
||||
<Heart className="w-16 h-16 text-primary mx-auto mb-4 fill-primary/10" />
|
||||
<CheckCircle className="w-10 h-10 text-primary mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-primary mb-2">{t.common.success}</h2>
|
||||
<p className="text-muted-foreground text-lg mb-6">{t.donate.successMessage}</p>
|
||||
<p className="text-sm font-mono text-muted-foreground bg-muted/30 px-4 py-2 rounded-lg inline-block">
|
||||
{request.caseId}
|
||||
</p>
|
||||
<div className="mt-8 flex gap-3 justify-center">
|
||||
<div className="relative min-h-[70vh] overflow-hidden">
|
||||
{/* Faint EHSAN geometric pattern */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-72 opacity-[0.06]"
|
||||
style={{ backgroundImage: PATTERN_BG, backgroundSize: "120px 120px" }}
|
||||
/>
|
||||
|
||||
<div className="container relative mx-auto px-4 py-16 max-w-xl text-center">
|
||||
{/* Checkmark badge */}
|
||||
<div className="mx-auto mb-8 flex h-20 w-20 items-center justify-center rounded-full bg-[#E9F5EF]">
|
||||
<Check className="h-9 w-9 text-[#176B43]" strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-foreground mb-3">{t.donate.successTitle}</h2>
|
||||
<p className="text-muted-foreground mb-7">{t.donate.successSubtitle}</p>
|
||||
|
||||
{/* Amount */}
|
||||
<div className="mb-9 flex items-center justify-center gap-2 text-4xl font-bold text-[#176B43]">
|
||||
<Riyal size="1em" />
|
||||
<span>
|
||||
{donatedAmount.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Receipt + reference chips */}
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(receiptNo, "receipt")}
|
||||
data-testid="button-copy-receipt"
|
||||
className="group flex w-full items-center justify-between gap-3 rounded-full border border-[#CDE7DA] bg-white px-5 py-3.5 text-start transition-colors hover:bg-[#F4FAF7]"
|
||||
>
|
||||
<Copy className="h-5 w-5 shrink-0 text-[#176B43]" />
|
||||
<span className="flex-1 text-sm text-foreground">
|
||||
<span className="text-muted-foreground">{t.donate.receiptNumber}: </span>
|
||||
<span className="font-medium">{receiptNo}</span>
|
||||
</span>
|
||||
{copiedField === "receipt" && (
|
||||
<span className="shrink-0 text-xs font-medium text-[#176B43]">{t.donate.copied}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(referenceNo, "reference")}
|
||||
data-testid="button-copy-reference"
|
||||
className="group flex w-full items-center justify-between gap-3 rounded-2xl border border-[#CDE7DA] bg-white px-5 py-3.5 text-start transition-colors hover:bg-[#F4FAF7]"
|
||||
>
|
||||
<Copy className="h-5 w-5 shrink-0 text-[#176B43]" />
|
||||
<span className="flex-1 text-sm text-foreground">
|
||||
<span className="text-muted-foreground">{t.donate.referenceNumber}: </span>
|
||||
<span className="font-medium break-all">{referenceNo}</span>
|
||||
</span>
|
||||
{copiedField === "reference" && (
|
||||
<span className="shrink-0 text-xs font-medium text-[#176B43]">{t.donate.copied}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Refund note */}
|
||||
<div className="mt-6 flex items-center justify-center gap-2 rounded-xl bg-muted/40 px-5 py-4 text-sm text-muted-foreground">
|
||||
<Info className="h-4 w-4 shrink-0" />
|
||||
<span>{t.donate.refundNote}</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="mt-10 flex gap-3 justify-center">
|
||||
<Button variant="outline" onClick={() => setLocation("/opportunities")}>
|
||||
{t.common.opportunities}
|
||||
</Button>
|
||||
@@ -106,8 +216,7 @@ export default function Donate() {
|
||||
{t.common.trackCase}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -143,6 +252,9 @@ export default function Donate() {
|
||||
queryClient.invalidateQueries({ queryKey: getListPublishedRequestsQueryKey() });
|
||||
queryClient.invalidateQueries({ queryKey: getGetRequestQueryKey(params.id || "") });
|
||||
removeFromCart(params.id || "");
|
||||
setDonatedAmount(Number(amount));
|
||||
setReceiptNo(generateReceiptNo());
|
||||
setReferenceNo(generateReferenceNo());
|
||||
setDonated(true);
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user