Complete EHSAN POC: fix all code review findings

- API routes: explicit return types on all Express handlers (fixes TS7030),
  `beneficiaryName` now accepted and stored in /thank-you route per OpenAPI spec.

- Home page: added search bar (filters by case ID, description, name) +
  Featured Opportunities section with live cards, progress bars, and Donate buttons.

- Opportunities page: added need-type filter pill bar (all 8 types + "All Types")
  with active state highlighting; empty state respects selected filter.

- i18n: expanded translations with all previously hardcoded strings
  (trackCase, notFound, noData, currentStep, search, searchPlaceholder,
  featuredTitle, noResults, donate.caseSummary, donate.caseNotFound,
  admin.noRequests, admin.needType, admin.amount, admin.track, admin.whatsapp,
  track.caseInfo, track.rejected, track.currentStepLabel, track.submitThankYou,
  thankYou.successNote, thankYou.beneficiaryMessageLabel,
  whatsapp.donorPhone, whatsapp.beneficiaryMessage, whatsapp.noEntries,
  opportunities.noOpportunities, opportunities.verified).
  All pages now use t.* — zero hardcoded English UI strings.

- TypeScript: both frontend (tsc --noEmit) and API server build are clean.
This commit is contained in:
Replit Agent
2026-06-05 17:12:44 +00:00
parent 12111a9562
commit 1dcfa0bfa5
9 changed files with 640 additions and 258 deletions
+42 -10
View File
@@ -4,7 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useParams, useLocation } from "wouter";
import { useLanguage } from "../contexts/LanguageContext";
import { useGetRequest, useDonateToRequest, getListRequestsQueryKey, getListPublishedRequestsQueryKey, getGetRequestQueryKey } from "@workspace/api-client-react";
import {
useGetRequest, useDonateToRequest,
getListRequestsQueryKey, getListPublishedRequestsQueryKey, getGetRequestQueryKey,
} from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@@ -14,6 +17,7 @@ import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { CheckCircle, Heart } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { Link } from "wouter";
const schema = z.object({
donorName: z.string().min(2),
@@ -43,7 +47,7 @@ export default function Donate() {
donorName: "",
donorPhone: "",
donorEmail: "",
amount: request?.requestedAmount || 0,
amount: 0,
},
});
@@ -80,7 +84,7 @@ export default function Donate() {
if (!request) {
return (
<div className="container mx-auto px-4 py-12 text-center text-muted-foreground">
Case not found.
{t.donate.caseNotFound}
</div>
);
}
@@ -94,10 +98,16 @@ export default function Donate() {
<CheckCircle className="w-10 h-10 text-green-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-green-700 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>
<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">
<Button variant="outline" onClick={() => setLocation("/opportunities")}>{t.common.opportunities}</Button>
<Button onClick={() => setLocation(`/track/${request.id}`)}>Track Case</Button>
<Button variant="outline" onClick={() => setLocation("/opportunities")}>
{t.common.opportunities}
</Button>
<Button onClick={() => setLocation(`/track/${request.id}`)}>
{t.common.trackCase}
</Button>
</div>
</CardContent>
</Card>
@@ -105,7 +115,12 @@ export default function Donate() {
);
}
const progress = Math.min(100, Math.round((request.collectedAmount / request.requestedAmount) * 100));
const progress = Math.min(
100,
request.requestedAmount > 0
? Math.round((request.collectedAmount / request.requestedAmount) * 100)
: 0
);
return (
<div className="container mx-auto px-4 py-12 max-w-2xl">
@@ -115,7 +130,12 @@ export default function Donate() {
{/* Case Summary */}
<Card className="mb-6 bg-primary/5 border-primary/20">
<CardContent className="pt-5 pb-5">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
{t.donate.caseSummary}
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="flex justify-between items-start mb-3">
<div>
<p className="font-semibold text-foreground">{request.description}</p>
@@ -126,8 +146,14 @@ export default function Donate() {
</Badge>
</div>
<div className="flex justify-between text-sm mb-2">
<span className="text-muted-foreground">{t.opportunities.collected}: <strong>{request.collectedAmount} </strong></span>
<span className="text-muted-foreground">{t.opportunities.target}: <strong>{request.requestedAmount} </strong></span>
<span className="text-muted-foreground">
{t.opportunities.collected}:{" "}
<strong>{request.collectedAmount.toLocaleString()} </strong>
</span>
<span className="text-muted-foreground">
{t.opportunities.target}:{" "}
<strong>{request.requestedAmount.toLocaleString()} </strong>
</span>
</div>
<Progress value={progress} className="h-2" />
</CardContent>
@@ -205,6 +231,12 @@ export default function Donate() {
</Form>
</CardContent>
</Card>
<div className="mt-4 text-center">
<Link href="/opportunities">
<Button variant="ghost" size="sm">{t.common.back}</Button>
</Link>
</div>
</div>
);
}