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:
@@ -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: "سلة تبرعاتك",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user