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
|
**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.
|
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",
|
backToDetails: "Back to Details",
|
||||||
paymentTitle: "Payment Details",
|
paymentTitle: "Payment Details",
|
||||||
selectAmountError: "Please select or enter a valid amount.",
|
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: {
|
cart: {
|
||||||
title: "Your Donation Cart",
|
title: "Your Donation Cart",
|
||||||
@@ -806,6 +812,12 @@ export const ar = {
|
|||||||
backToDetails: "رجوع للتفاصيل",
|
backToDetails: "رجوع للتفاصيل",
|
||||||
paymentTitle: "بيانات الدفع",
|
paymentTitle: "بيانات الدفع",
|
||||||
selectAmountError: "الرجاء اختيار أو إدخال مبلغ صحيح.",
|
selectAmountError: "الرجاء اختيار أو إدخال مبلغ صحيح.",
|
||||||
|
successTitle: "شكرا على تبرعك الكريم",
|
||||||
|
successSubtitle: "لقد تم إتمام عملية تبرعك بنجاح!",
|
||||||
|
receiptNumber: "رقم الإيصال",
|
||||||
|
referenceNumber: "الرقم المرجعي للعملية",
|
||||||
|
refundNote: "لتتم عملية الإسترداد بسهولة، نأمل حفظ الرقم المرجعي للعملية",
|
||||||
|
copied: "تم النسخ",
|
||||||
},
|
},
|
||||||
cart: {
|
cart: {
|
||||||
title: "سلة تبرعاتك",
|
title: "سلة تبرعاتك",
|
||||||
|
|||||||
@@ -15,13 +15,38 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
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 { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { getNeedImage } from "../lib/needImages";
|
import { getNeedImage } from "../lib/needImages";
|
||||||
import { Riyal } from "@/components/Riyal";
|
import { Riyal } from "@/components/Riyal";
|
||||||
|
|
||||||
const PRESETS = [100, 50, 10];
|
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({
|
const schema = z.object({
|
||||||
donorName: z.string().min(2),
|
donorName: z.string().min(2),
|
||||||
donorPhone: z.string().min(10),
|
donorPhone: z.string().min(10),
|
||||||
@@ -49,6 +74,31 @@ export default function Donate() {
|
|||||||
const [onBehalf, setOnBehalf] = useState(false);
|
const [onBehalf, setOnBehalf] = useState(false);
|
||||||
const [onBehalfName, setOnBehalfName] = useState("");
|
const [onBehalfName, setOnBehalfName] = useState("");
|
||||||
const [donated, setDonated] = useState(false);
|
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 || "", {
|
const { data: request, isLoading } = useGetRequest(params.id || "", {
|
||||||
query: { enabled: !!params.id, queryKey: getGetRequestQueryKey(params.id || "") },
|
query: { enabled: !!params.id, queryKey: getGetRequestQueryKey(params.id || "") },
|
||||||
@@ -88,17 +138,77 @@ export default function Donate() {
|
|||||||
|
|
||||||
if (donated) {
|
if (donated) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12 max-w-2xl">
|
<div className="relative min-h-[70vh] overflow-hidden">
|
||||||
<Card className="border-2 border-primary/20">
|
{/* Faint EHSAN geometric pattern */}
|
||||||
<CardContent className="pt-10 pb-10 text-center">
|
<div
|
||||||
<Heart className="w-16 h-16 text-primary mx-auto mb-4 fill-primary/10" />
|
aria-hidden="true"
|
||||||
<CheckCircle className="w-10 h-10 text-primary mx-auto mb-4" />
|
className="pointer-events-none absolute inset-x-0 top-0 h-72 opacity-[0.06]"
|
||||||
<h2 className="text-2xl font-bold text-primary mb-2">{t.common.success}</h2>
|
style={{ backgroundImage: PATTERN_BG, backgroundSize: "120px 120px" }}
|
||||||
<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}
|
<div className="container relative mx-auto px-4 py-16 max-w-xl text-center">
|
||||||
</p>
|
{/* Checkmark badge */}
|
||||||
<div className="mt-8 flex gap-3 justify-center">
|
<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")}>
|
<Button variant="outline" onClick={() => setLocation("/opportunities")}>
|
||||||
{t.common.opportunities}
|
{t.common.opportunities}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -106,8 +216,7 @@ export default function Donate() {
|
|||||||
{t.common.trackCase}
|
{t.common.trackCase}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143,6 +252,9 @@ export default function Donate() {
|
|||||||
queryClient.invalidateQueries({ queryKey: getListPublishedRequestsQueryKey() });
|
queryClient.invalidateQueries({ queryKey: getListPublishedRequestsQueryKey() });
|
||||||
queryClient.invalidateQueries({ queryKey: getGetRequestQueryKey(params.id || "") });
|
queryClient.invalidateQueries({ queryKey: getGetRequestQueryKey(params.id || "") });
|
||||||
removeFromCart(params.id || "");
|
removeFromCart(params.id || "");
|
||||||
|
setDonatedAmount(Number(amount));
|
||||||
|
setReceiptNo(generateReceiptNo());
|
||||||
|
setReferenceNo(generateReferenceNo());
|
||||||
setDonated(true);
|
setDonated(true);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user