8519202949
Follow-up tweaks so the EHSAN POC matches the official ehsan.sa site. - Font: switched from Tajawal to IBM Plex Sans Arabic (index.html + index.css). ehsan.sa's exact webfont couldn't be auto-detected (site blocks scraping; no Wayback snapshot), so picked the closest official match. - Home hero: replaced the gray search-box hero with a full-bleed green branded banner (badge, title, subtitle, two CTAs, decorative leaf SVGs), matching ehsan.sa. Moved the search bar above the featured opportunities grid (with an sr-only label for accessibility). - Currency: replaced the legacy "﷼" glyph everywhere with the new official Saudi Riyal symbol via a reusable <Riyal /> component that masks a processed PNG (src/assets/riyal.png) colored with currentColor; marked aria-hidden since the adjacent number conveys the value. Applied across home stats, OpportunityCard, donate, track, admin, request. - Added AR+EN translation keys heroBadge/heroBrowse. Verified: tsc clean, no console errors, screenshots confirm hero, font, and riyal symbol render correctly. Code review fixes applied (search label, decorative riyal aria, removed unused key).
270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { useLanguage } from "../contexts/LanguageContext";
|
|
import { useCreateRequest } from "@workspace/api-client-react";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { CheckCircle, XCircle, Clock } from "lucide-react";
|
|
import { Riyal } from "@/components/Riyal";
|
|
|
|
const schema = z.object({
|
|
beneficiaryName: z.string().min(2),
|
|
nationalId: z.string().min(10).max(10),
|
|
phone: z.string().min(10),
|
|
source: z.enum(["beneficiary", "charity", "official"]),
|
|
sourceName: z.string().min(2),
|
|
needType: z.enum(["electricity", "water", "food", "health", "housing", "refrigerator", "air_conditioner", "court_order"]),
|
|
requestedAmount: z.coerce.number().positive(),
|
|
description: z.string().min(20),
|
|
});
|
|
|
|
type FormData = z.infer<typeof schema>;
|
|
|
|
export default function RequestSupport() {
|
|
const { t } = useLanguage();
|
|
const [submitted, setSubmitted] = useState<any>(null);
|
|
const createRequest = useCreateRequest();
|
|
|
|
const form = useForm<FormData>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: {
|
|
beneficiaryName: "",
|
|
nationalId: "",
|
|
phone: "",
|
|
source: "beneficiary",
|
|
sourceName: "",
|
|
needType: "electricity",
|
|
requestedAmount: 0,
|
|
description: "",
|
|
},
|
|
});
|
|
|
|
const onSubmit = (data: FormData) => {
|
|
createRequest.mutate(
|
|
{ data },
|
|
{
|
|
onSuccess: (result) => {
|
|
setSubmitted(result);
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
if (submitted) {
|
|
const isVerified = submitted.status === "verified";
|
|
const isRejected = submitted.status === "rejected";
|
|
const isPending = submitted.status === "pending_review";
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-12 max-w-2xl">
|
|
<Card className="border-2">
|
|
<CardContent className="pt-8 pb-8 text-center">
|
|
{isVerified && (
|
|
<div className="flex flex-col items-center gap-4">
|
|
<CheckCircle className="w-16 h-16 text-green-600" />
|
|
<h2 className="text-2xl font-bold text-green-700">{isVerified ? (t as any).statuses.verified : ""}</h2>
|
|
<p className="text-muted-foreground">{t.request.submitSuccess}</p>
|
|
<Badge className="bg-green-600 text-white px-4 py-1 text-sm">{t.statuses.verified}</Badge>
|
|
</div>
|
|
)}
|
|
{isPending && (
|
|
<div className="flex flex-col items-center gap-4">
|
|
<Clock className="w-16 h-16 text-amber-500" />
|
|
<h2 className="text-2xl font-bold text-amber-600">{t.statuses.pending_review}</h2>
|
|
<p className="text-muted-foreground">{t.request.submitSuccess}</p>
|
|
<Badge variant="secondary" className="px-4 py-1 text-sm">{t.statuses.pending_review}</Badge>
|
|
</div>
|
|
)}
|
|
{isRejected && (
|
|
<div className="flex flex-col items-center gap-4">
|
|
<XCircle className="w-16 h-16 text-red-500" />
|
|
<h2 className="text-2xl font-bold text-red-600">{t.statuses.rejected}</h2>
|
|
<p className="text-muted-foreground">{submitted.rejectionReason}</p>
|
|
<Badge variant="destructive" className="px-4 py-1 text-sm">{t.statuses.rejected}</Badge>
|
|
</div>
|
|
)}
|
|
<div className="mt-6 p-4 bg-muted/30 rounded-lg text-sm text-start">
|
|
<p className="font-medium mb-1">{submitted.caseId}</p>
|
|
<p className="text-muted-foreground">{submitted.beneficiaryName}</p>
|
|
</div>
|
|
<Button className="mt-6" onClick={() => { setSubmitted(null); form.reset(); }}>
|
|
{t.common.back}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-12 max-w-2xl">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-foreground">{t.request.title}</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
{t.home.heroSubtitle}
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-semibold text-primary">{t.request.title}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<FormField
|
|
control={form.control}
|
|
name="beneficiaryName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.beneficiaryName}</FormLabel>
|
|
<FormControl>
|
|
<Input data-testid="input-beneficiaryName" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="nationalId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.nationalId}</FormLabel>
|
|
<FormControl>
|
|
<Input data-testid="input-nationalId" maxLength={10} {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="phone"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.phone}</FormLabel>
|
|
<FormControl>
|
|
<Input data-testid="input-phone" type="tel" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<FormField
|
|
control={form.control}
|
|
name="source"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.source}</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger data-testid="select-source">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="beneficiary">{t.sources.beneficiary}</SelectItem>
|
|
<SelectItem value="charity">{t.sources.charity}</SelectItem>
|
|
<SelectItem value="official">{t.sources.official}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="sourceName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.sourceName}</FormLabel>
|
|
<FormControl>
|
|
<Input data-testid="input-sourceName" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<FormField
|
|
control={form.control}
|
|
name="needType"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.needType}</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger data-testid="select-needType">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{Object.entries(t.needTypes).map(([key, label]) => (
|
|
<SelectItem key={key} value={key}>{label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="requestedAmount"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="inline-flex items-center gap-1">{t.request.amount} (<Riyal />)</FormLabel>
|
|
<FormControl>
|
|
<Input data-testid="input-amount" type="number" min={1} {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t.request.description}</FormLabel>
|
|
<FormControl>
|
|
<Textarea data-testid="input-description" rows={4} {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
disabled={createRequest.isPending}
|
|
data-testid="button-submit"
|
|
>
|
|
{createRequest.isPending ? t.common.loading : t.common.submit}
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|