Display dynamic donation statistics and update translations

Implement dynamic, hash-derived statistics for visits, last donation, beneficiaries, and donations on the donate page. Update English and Arabic translations to support these new statistics.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 1fa9329f-0cec-4a2f-80e8-e26dbae3142e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d09ce5e5-3522-4026-98f7-5e4e673f3a38
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/4d696b13-86f2-4c9d-be0d-95b293430047/1fa9329f-0cec-4a2f-80e8-e26dbae3142e/3JkYdFP
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
Replit Agent
2026-06-06 09:37:53 +00:00
parent e7f0995f1d
commit ea4134f94e
3 changed files with 84 additions and 2 deletions
@@ -363,6 +363,14 @@ export const en = {
referenceNumber: "Transaction Reference Number",
refundNote: "To make refunds easy, please keep the transaction reference number.",
copied: "Copied",
statsVisits: "Visits",
statsVisitsUnit: "visits",
statsLastDonation: "Last donation",
statsSecondUnit: "seconds ago",
statsBeneficiaries: "Beneficiaries",
statsOutOf: "of",
statsDonations: "Donations",
statsDonationsUnit: "donations",
},
cart: {
title: "Your Donation Cart",
@@ -818,6 +826,14 @@ export const ar = {
referenceNumber: "الرقم المرجعي للعملية",
refundNote: "لتتم عملية الإسترداد بسهولة، نأمل حفظ الرقم المرجعي للعملية",
copied: "تم النسخ",
statsVisits: "الزيارات",
statsVisitsUnit: "زيارة",
statsLastDonation: "آخر عملية تبرع قبل",
statsSecondUnit: "ثانية",
statsBeneficiaries: "عدد المستفيدين",
statsOutOf: "من أصل",
statsDonations: "عدد عمليات التبرع",
statsDonationsUnit: "عملية",
},
cart: {
title: "سلة تبرعاتك",
+68 -2
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -15,7 +15,7 @@ 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 { Gift, Check, Copy, Info } from "lucide-react";
import { Gift, Check, Copy, Info, Eye, Clock, Users, Radio } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { getNeedImage } from "../lib/needImages";
import { Riyal } from "@/components/Riyal";
@@ -47,6 +47,13 @@ const PATTERN_SVG = encodeURIComponent(
);
const PATTERN_BG = `url("data:image/svg+xml,${PATTERN_SVG}")`;
// Stable per-case pseudo-random seed so POC stat values don't flicker on re-render.
function hashStr(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
return h;
}
const schema = z.object({
donorName: z.string().min(2),
donorPhone: z.string().min(10),
@@ -111,6 +118,19 @@ export default function Donate() {
defaultValues: { donorName: "", donorPhone: "", donorEmail: "" },
});
// POC demo stats — stable per case (visits / last-donation time / beneficiaries
// are not stored by the API, so derive plausible values from the case id).
const stats = useMemo(() => {
const h = hashStr(params.id || "case");
return {
visits: 8000 + (h % 15000),
donations: 1500 + ((h >> 3) % 22000),
beneficiaries: 5 + (h % 30),
totalBeneficiaries: 40,
lastDonationSeconds: 11 + (h % 49),
};
}, [params.id]);
if (isLoading) {
return (
<div className="container mx-auto px-4 py-12 max-w-5xl space-y-4">
@@ -496,6 +516,52 @@ export default function Donate() {
)}
</div>
{/* Case stat cards (POC demo values) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
{[
{
icon: Eye,
label: t.donate.statsVisits,
value: stats.visits.toLocaleString("en-US"),
unit: t.donate.statsVisitsUnit,
},
{
icon: Clock,
label: t.donate.statsLastDonation,
value: stats.lastDonationSeconds.toLocaleString("en-US"),
unit: t.donate.statsSecondUnit,
},
{
icon: Users,
label: t.donate.statsBeneficiaries,
value: stats.beneficiaries.toLocaleString("en-US"),
unit: `${t.donate.statsOutOf} ${stats.totalBeneficiaries.toLocaleString("en-US")}`,
},
{
icon: Radio,
label: t.donate.statsDonations,
value: stats.donations.toLocaleString("en-US"),
unit: t.donate.statsDonationsUnit,
},
].map(({ icon: Icon, label, value, unit }) => (
<div
key={label}
className="flex items-center justify-between gap-4 rounded-2xl border border-[#E3EEE8] bg-white p-5"
>
<div className="min-w-0">
<p className="text-sm text-[#1B8354] mb-2">{label}</p>
<p className="flex items-baseline gap-1.5 flex-wrap font-bold text-foreground text-xl">
<span>{value}</span>
<span className="text-xs font-normal text-[#1B8354]">{unit}</span>
</p>
</div>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-[#EAF5EF] text-[#1B8354]">
<Icon className="h-5 w-5" />
</div>
</div>
))}
</div>
<div className="mt-6 text-center">
<Link href="/opportunities">
<Button variant="ghost" size="sm">{t.common.back}</Button>
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB