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",
|
referenceNumber: "Transaction Reference Number",
|
||||||
refundNote: "To make refunds easy, please keep the transaction reference number.",
|
refundNote: "To make refunds easy, please keep the transaction reference number.",
|
||||||
copied: "Copied",
|
copied: "Copied",
|
||||||
|
statsVisits: "Visits",
|
||||||
|
statsVisitsUnit: "visits",
|
||||||
|
statsLastDonation: "Last donation",
|
||||||
|
statsSecondUnit: "seconds ago",
|
||||||
|
statsBeneficiaries: "Beneficiaries",
|
||||||
|
statsOutOf: "of",
|
||||||
|
statsDonations: "Donations",
|
||||||
|
statsDonationsUnit: "donations",
|
||||||
},
|
},
|
||||||
cart: {
|
cart: {
|
||||||
title: "Your Donation Cart",
|
title: "Your Donation Cart",
|
||||||
@@ -818,6 +826,14 @@ export const ar = {
|
|||||||
referenceNumber: "الرقم المرجعي للعملية",
|
referenceNumber: "الرقم المرجعي للعملية",
|
||||||
refundNote: "لتتم عملية الإسترداد بسهولة، نأمل حفظ الرقم المرجعي للعملية",
|
refundNote: "لتتم عملية الإسترداد بسهولة، نأمل حفظ الرقم المرجعي للعملية",
|
||||||
copied: "تم النسخ",
|
copied: "تم النسخ",
|
||||||
|
statsVisits: "الزيارات",
|
||||||
|
statsVisitsUnit: "زيارة",
|
||||||
|
statsLastDonation: "آخر عملية تبرع قبل",
|
||||||
|
statsSecondUnit: "ثانية",
|
||||||
|
statsBeneficiaries: "عدد المستفيدين",
|
||||||
|
statsOutOf: "من أصل",
|
||||||
|
statsDonations: "عدد عمليات التبرع",
|
||||||
|
statsDonationsUnit: "عملية",
|
||||||
},
|
},
|
||||||
cart: {
|
cart: {
|
||||||
title: "سلة تبرعاتك",
|
title: "سلة تبرعاتك",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -15,7 +15,7 @@ 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 { 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 { 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";
|
||||||
@@ -47,6 +47,13 @@ const PATTERN_SVG = encodeURIComponent(
|
|||||||
);
|
);
|
||||||
const PATTERN_BG = `url("data:image/svg+xml,${PATTERN_SVG}")`;
|
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({
|
const schema = z.object({
|
||||||
donorName: z.string().min(2),
|
donorName: z.string().min(2),
|
||||||
donorPhone: z.string().min(10),
|
donorPhone: z.string().min(10),
|
||||||
@@ -111,6 +118,19 @@ export default function Donate() {
|
|||||||
defaultValues: { donorName: "", donorPhone: "", donorEmail: "" },
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12 max-w-5xl space-y-4">
|
<div className="container mx-auto px-4 py-12 max-w-5xl space-y-4">
|
||||||
@@ -496,6 +516,52 @@ export default function Donate() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div className="mt-6 text-center">
|
||||||
<Link href="/opportunities">
|
<Link href="/opportunities">
|
||||||
<Button variant="ghost" size="sm">{t.common.back}</Button>
|
<Button variant="ghost" size="sm">{t.common.back}</Button>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Reference in New Issue
Block a user