Match ehsan.sa look: font, hero, and new Riyal symbol
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).
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
- [Donate semantics](donate-semantics.md) — donations accumulate + clamp to target; a case enters the closed-loop pipeline only when fully funded.
|
- [Donate semantics](donate-semantics.md) — donations accumulate + clamp to target; a case enters the closed-loop pipeline only when fully funded.
|
||||||
- [api-server data](api-server-data.md) — mockDb is in-memory and mutated by POST calls; restart the workflow to reset to clean seed for demos.
|
- [api-server data](api-server-data.md) — mockDb is in-memory and mutated by POST calls; restart the workflow to reset to clean seed for demos.
|
||||||
|
- [EHSAN branding](ehsan-branding.md) — ehsan.sa blocks scraping; font is IBM Plex Sans Arabic (best match), currency uses new Saudi Riyal symbol via <Riyal/> mask component.
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: EHSAN branding (ehsan.sa)
|
||||||
|
description: Font, currency symbol, and hero conventions for matching the official ehsan.sa look.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Matching ehsan.sa
|
||||||
|
|
||||||
|
- **Font:** the POC uses `IBM Plex Sans Arabic` (Google Fonts) as the closest official-looking match. ehsan.sa's exact webfont could NOT be auto-detected — the live site blocks server-side access (Firecrawl returns ERR_TUNNEL_CONNECTION_FAILED, curl with a browser UA returns nothing, and the Wayback Machine has no snapshot). If the font ever needs to be exact, ask the user to read it from browser DevTools (Computed > font-family).
|
||||||
|
- **Currency = new Saudi Riyal symbol**, not the legacy `﷼` glyph and not the word "ريال". It is rendered by the `<Riyal />` component (`src/components/Riyal.tsx`) which masks a processed PNG (`src/assets/riyal.png`) with `mask-image` + `background-color: currentColor`, so it inherits the surrounding text color and font size. The symbol is decorative (`aria-hidden`) since the adjacent number conveys the value.
|
||||||
|
|
||||||
|
**Why:** the user explicitly flagged the wrong font, the wrong currency symbol, and a non-official hero. The riyal mask approach was chosen because font/Unicode support for the new symbol is unreliable; masking the user-provided glyph guarantees it matches their reference and recolors anywhere.
|
||||||
|
|
||||||
|
**How to apply:** put the amount number first, then `<Riyal />` (e.g. `{amount.toLocaleString()} <Riyal />`). The home hero should be a full-bleed green (`bg-primary`) banner with a badge, title/subtitle, and CTAs — not a gray search box.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
modules = ["nodejs-24"]
|
modules = ["nodejs-24", "python-3.11"]
|
||||||
|
|
||||||
[deployment]
|
[deployment]
|
||||||
router = "application"
|
router = "application"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Share2, ShoppingCart } from "lucide-react";
|
import { Share2, ShoppingCart } from "lucide-react";
|
||||||
import { getNeedImage } from "../lib/needImages";
|
import { getNeedImage } from "../lib/needImages";
|
||||||
|
import { Riyal } from "@/components/Riyal";
|
||||||
|
|
||||||
interface OpportunityCardProps {
|
interface OpportunityCardProps {
|
||||||
request: {
|
request: {
|
||||||
@@ -17,8 +18,6 @@ interface OpportunityCardProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const RIYAL = "﷼";
|
|
||||||
|
|
||||||
export function OpportunityCard({ request }: OpportunityCardProps) {
|
export function OpportunityCard({ request }: OpportunityCardProps) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
@@ -78,14 +77,14 @@ export function OpportunityCard({ request }: OpportunityCardProps) {
|
|||||||
<div className="grid grid-cols-2 gap-2 bg-muted/40 rounded-xl p-4 mb-4 mt-auto">
|
<div className="grid grid-cols-2 gap-2 bg-muted/40 rounded-xl p-4 mb-4 mt-auto">
|
||||||
<div className="text-center border-s border-border">
|
<div className="text-center border-s border-border">
|
||||||
<p className="text-sm text-primary mb-1">{t.opportunities.remainingShort}</p>
|
<p className="text-sm text-primary mb-1">{t.opportunities.remainingShort}</p>
|
||||||
<p className="font-bold text-foreground">
|
<p className="font-bold text-foreground inline-flex items-center gap-1">
|
||||||
{RIYAL} {remaining.toLocaleString()}
|
{remaining.toLocaleString()} <Riyal />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-primary mb-1">{t.opportunities.collectedShort}</p>
|
<p className="text-sm text-primary mb-1">{t.opportunities.collectedShort}</p>
|
||||||
<p className="font-bold text-foreground">
|
<p className="font-bold text-foreground inline-flex items-center gap-1">
|
||||||
{RIYAL} {request.collectedAmount.toLocaleString()}
|
{request.collectedAmount.toLocaleString()} <Riyal />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +112,7 @@ export function OpportunityCard({ request }: OpportunityCardProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<span className="absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm pointer-events-none">
|
<span className="absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm pointer-events-none">
|
||||||
{RIYAL}
|
<Riyal />
|
||||||
</span>
|
</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import riyalUrl from "@/assets/riyal.png";
|
||||||
|
|
||||||
|
interface RiyalProps {
|
||||||
|
className?: string;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Official new Saudi Riyal currency symbol.
|
||||||
|
* Rendered as a CSS mask so it inherits the current text color (currentColor).
|
||||||
|
*/
|
||||||
|
export function Riyal({ className = "", size = "0.95em" }: RiyalProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`inline-block shrink-0 align-[-0.12em] ${className}`}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
backgroundColor: "currentColor",
|
||||||
|
WebkitMaskImage: `url(${riyalUrl})`,
|
||||||
|
maskImage: `url(${riyalUrl})`,
|
||||||
|
WebkitMaskRepeat: "no-repeat",
|
||||||
|
maskRepeat: "no-repeat",
|
||||||
|
WebkitMaskPosition: "center",
|
||||||
|
maskPosition: "center",
|
||||||
|
WebkitMaskSize: "contain",
|
||||||
|
maskSize: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;800&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
--chart-4: 143 20% 80%;
|
--chart-4: 143 20% 80%;
|
||||||
--chart-5: 143 10% 90%;
|
--chart-5: 143 10% 90%;
|
||||||
|
|
||||||
--app-font-sans: 'Tajawal', sans-serif;
|
--app-font-sans: 'IBM Plex Sans Arabic', sans-serif;
|
||||||
--app-font-serif: Georgia, serif;
|
--app-font-serif: Georgia, serif;
|
||||||
--app-font-mono: Menlo, monospace;
|
--app-font-mono: Menlo, monospace;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ export const en = {
|
|||||||
searchOpportunities: "Search Donation Opportunities",
|
searchOpportunities: "Search Donation Opportunities",
|
||||||
searchLabel: "Find a cause to support",
|
searchLabel: "Find a cause to support",
|
||||||
searchButton: "Search",
|
searchButton: "Search",
|
||||||
|
heroBadge: "National Platform for Charitable Work",
|
||||||
|
heroBrowse: "Browse Opportunities",
|
||||||
featuredTitle: "Featured Opportunities",
|
featuredTitle: "Featured Opportunities",
|
||||||
noResults: "No opportunities match your search.",
|
noResults: "No opportunities match your search.",
|
||||||
},
|
},
|
||||||
@@ -270,6 +272,8 @@ export const ar = {
|
|||||||
searchOpportunities: "ابحث في فرص التبرع",
|
searchOpportunities: "ابحث في فرص التبرع",
|
||||||
searchLabel: "ابحث عن قضية لدعمها",
|
searchLabel: "ابحث عن قضية لدعمها",
|
||||||
searchButton: "بحث",
|
searchButton: "بحث",
|
||||||
|
heroBadge: "المنصة الوطنية للعمل الخيري",
|
||||||
|
heroBrowse: "تصفّح الفرص",
|
||||||
featuredTitle: "الفرص المميزة",
|
featuredTitle: "الفرص المميزة",
|
||||||
noResults: "لا توجد فرص تطابق بحثك.",
|
noResults: "لا توجد فرص تطابق بحثك.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
import { Riyal } from "@/components/Riyal";
|
||||||
|
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
@@ -70,7 +71,7 @@ export default function Admin() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{t.needTypes[req.needType as keyof typeof t.needTypes] || req.needType}
|
{t.needTypes[req.needType as keyof typeof t.needTypes] || req.needType}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{req.requestedAmount.toLocaleString()} ﷼</TableCell>
|
<TableCell><span className="inline-flex items-center gap-1">{req.requestedAmount.toLocaleString()} <Riyal /></span></TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="secondary" className="font-normal">
|
<Badge variant="secondary" className="font-normal">
|
||||||
{t.statuses[req.status as keyof typeof t.statuses] || req.status}
|
{t.statuses[req.status as keyof typeof t.statuses] || req.status}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { CheckCircle, Heart, Gift, ShoppingCart, Check } from "lucide-react";
|
import { CheckCircle, Heart, Gift, ShoppingCart, Check } 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";
|
||||||
|
|
||||||
const RIYAL = "﷼";
|
|
||||||
const PRESETS = [100, 50, 10];
|
const PRESETS = [100, 50, 10];
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@@ -203,11 +203,11 @@ export default function Donate() {
|
|||||||
<div className="grid grid-cols-2 gap-2 bg-muted/40 rounded-xl p-4">
|
<div className="grid grid-cols-2 gap-2 bg-muted/40 rounded-xl p-4">
|
||||||
<div className="text-center border-s border-border">
|
<div className="text-center border-s border-border">
|
||||||
<p className="text-sm text-primary mb-1">{t.opportunities.remainingShort}</p>
|
<p className="text-sm text-primary mb-1">{t.opportunities.remainingShort}</p>
|
||||||
<p className="font-bold text-foreground">{RIYAL} {remaining.toLocaleString()}</p>
|
<p className="font-bold text-foreground inline-flex items-center gap-1">{remaining.toLocaleString()} <Riyal /></p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-primary mb-1">{t.opportunities.collectedShort}</p>
|
<p className="text-sm text-primary mb-1">{t.opportunities.collectedShort}</p>
|
||||||
<p className="font-bold text-foreground">{RIYAL} {request.collectedAmount.toLocaleString()}</p>
|
<p className="font-bold text-foreground inline-flex items-center gap-1">{request.collectedAmount.toLocaleString()} <Riyal /></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -244,7 +244,7 @@ export default function Donate() {
|
|||||||
}`}
|
}`}
|
||||||
data-testid={`preset-${p}`}
|
data-testid={`preset-${p}`}
|
||||||
>
|
>
|
||||||
{RIYAL} {p}
|
<span className="inline-flex items-center justify-center gap-1">{p} <Riyal /></span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +252,7 @@ export default function Donate() {
|
|||||||
{/* Custom amount */}
|
{/* Custom amount */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm pointer-events-none">
|
<span className="absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm pointer-events-none">
|
||||||
{RIYAL}
|
<Riyal />
|
||||||
</span>
|
</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -313,7 +313,7 @@ export default function Donate() {
|
|||||||
<CardContent className="pt-5">
|
<CardContent className="pt-5">
|
||||||
<div className="flex items-center justify-between bg-primary/5 rounded-lg px-4 py-3 mb-5">
|
<div className="flex items-center justify-between bg-primary/5 rounded-lg px-4 py-3 mb-5">
|
||||||
<span className="text-sm text-muted-foreground">{t.donate.amount}</span>
|
<span className="text-sm text-muted-foreground">{t.donate.amount}</span>
|
||||||
<span className="text-lg font-bold text-primary">{RIYAL} {Number(amount).toLocaleString()}</span>
|
<span className="text-lg font-bold text-primary inline-flex items-center gap-1">{Number(amount).toLocaleString()} <Riyal /></span>
|
||||||
</div>
|
</div>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { OpportunityCard } from "../components/OpportunityCard";
|
import { OpportunityCard } from "../components/OpportunityCard";
|
||||||
|
import { Riyal } from "@/components/Riyal";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
|
|
||||||
@@ -26,37 +27,71 @@ export default function Home() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12">
|
<>
|
||||||
{/* Hero */}
|
{/* Hero — official EHSAN green banner */}
|
||||||
<section className="text-center py-16 bg-primary/5 rounded-3xl mb-12 border border-primary/10 px-6">
|
<section className="relative overflow-hidden bg-primary text-primary-foreground">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-primary mb-6">
|
<div
|
||||||
|
className="absolute inset-0 opacity-25"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle at 85% 20%, rgba(255,255,255,0.18) 0, transparent 45%), radial-gradient(circle at 10% 90%, rgba(0,0,0,0.18) 0, transparent 50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* decorative leaves (visual only) */}
|
||||||
|
<svg
|
||||||
|
className="pointer-events-none absolute -bottom-10 start-0 h-56 w-56 text-white/10"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M100 10C60 40 40 90 60 140c30-20 60-50 80-100C120 60 100 90 90 120c-5-40 0-80 10-110z" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
className="pointer-events-none absolute -top-12 end-8 h-72 w-72 text-white/10 rotate-180"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M100 10C60 40 40 90 60 140c30-20 60-50 80-100C120 60 100 90 90 120c-5-40 0-80 10-110z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-20 md:py-24 relative">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<span className="inline-block rounded-full bg-white/15 px-4 py-1 text-sm font-medium mb-5">
|
||||||
|
{t.home.heroBadge}
|
||||||
|
</span>
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold leading-tight mb-5">
|
||||||
{t.home.heroTitle}
|
{t.home.heroTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
|
<p className="text-lg md:text-xl text-primary-foreground/90 mb-8 leading-relaxed">
|
||||||
{t.home.heroSubtitle}
|
{t.home.heroSubtitle}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
{/* Search Bar */}
|
<Link href="/opportunities">
|
||||||
<div className="max-w-xl mx-auto">
|
<Button
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
size="lg"
|
||||||
{t.home.searchLabel}
|
className="bg-white text-primary hover:bg-white/90 font-bold px-7"
|
||||||
</label>
|
data-testid="button-heroBrowse"
|
||||||
<div className="flex gap-2">
|
>
|
||||||
<div className="relative flex-1">
|
{t.home.heroBrowse}
|
||||||
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
</Button>
|
||||||
<Input
|
</Link>
|
||||||
className="ps-9"
|
<Link href="/request">
|
||||||
placeholder={t.common.searchPlaceholder}
|
<Button
|
||||||
value={query}
|
size="lg"
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
variant="outline"
|
||||||
data-testid="input-search"
|
className="border-white/70 bg-transparent text-white hover:bg-white/10 font-bold px-7"
|
||||||
/>
|
data-testid="button-heroRequest"
|
||||||
|
>
|
||||||
|
{t.nav.requestSupport}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Button data-testid="button-search">{t.home.searchButton}</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-12">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
||||||
@@ -85,8 +120,8 @@ export default function Home() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-4xl font-bold text-primary">
|
<div className="text-4xl font-bold text-primary inline-flex items-center gap-2">
|
||||||
{stats?.totalCollected?.toLocaleString() || 0} ﷼
|
{stats?.totalCollected?.toLocaleString() || 0} <Riyal />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -114,6 +149,25 @@ export default function Home() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="mb-8 max-w-xl">
|
||||||
|
<label htmlFor="home-search" className="sr-only">{t.home.searchLabel}</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="home-search"
|
||||||
|
className="ps-9"
|
||||||
|
placeholder={t.common.searchPlaceholder}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
data-testid="input-search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button data-testid="button-search">{t.home.searchButton}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{pubLoading ? (
|
{pubLoading ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-96 w-full" />)}
|
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-96 w-full" />)}
|
||||||
@@ -150,5 +204,6 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { CheckCircle, XCircle, Clock } from "lucide-react";
|
import { CheckCircle, XCircle, Clock } from "lucide-react";
|
||||||
|
import { Riyal } from "@/components/Riyal";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
beneficiaryName: z.string().min(2),
|
beneficiaryName: z.string().min(2),
|
||||||
@@ -227,7 +228,7 @@ export default function RequestSupport() {
|
|||||||
name="requestedAmount"
|
name="requestedAmount"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t.request.amount} (﷼)</FormLabel>
|
<FormLabel className="inline-flex items-center gap-1">{t.request.amount} (<Riyal />)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input data-testid="input-amount" type="number" min={1} {...field} />
|
<Input data-testid="input-amount" type="number" min={1} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Check, Clock } from "lucide-react";
|
import { Check, Clock } from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
import { Riyal } from "@/components/Riyal";
|
||||||
|
|
||||||
const STEPS = [
|
const STEPS = [
|
||||||
"step1", "step2", "step3", "step4", "step5",
|
"step1", "step2", "step3", "step4", "step5",
|
||||||
@@ -100,13 +101,13 @@ export default function Track() {
|
|||||||
<p className="text-muted-foreground text-xs uppercase tracking-wide mb-1">
|
<p className="text-muted-foreground text-xs uppercase tracking-wide mb-1">
|
||||||
{t.request.amount}
|
{t.request.amount}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold">{request.requestedAmount.toLocaleString()} ﷼</p>
|
<p className="font-semibold inline-flex items-center gap-1">{request.requestedAmount.toLocaleString()} <Riyal /></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs uppercase tracking-wide mb-1">
|
<p className="text-muted-foreground text-xs uppercase tracking-wide mb-1">
|
||||||
{t.opportunities.collected}
|
{t.opportunities.collected}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold text-primary">{request.collectedAmount.toLocaleString()} ﷼</p>
|
<p className="font-semibold text-primary inline-flex items-center gap-1">{request.collectedAmount.toLocaleString()} <Riyal /></p>
|
||||||
</div>
|
</div>
|
||||||
{request.donorName && (
|
{request.donorName && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Reference in New Issue
Block a user