diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index 367778f..3c2a28a 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -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. - [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 mask component. diff --git a/.agents/memory/ehsan-branding.md b/.agents/memory/ehsan-branding.md new file mode 100644 index 0000000..43e1660 --- /dev/null +++ b/.agents/memory/ehsan-branding.md @@ -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 `` 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 `` (e.g. `{amount.toLocaleString()} `). The home hero should be a full-bleed green (`bg-primary`) banner with a badge, title/subtitle, and CTAs — not a gray search box. diff --git a/.replit b/.replit index ade20b3..b4c57e8 100644 --- a/.replit +++ b/.replit @@ -1,4 +1,4 @@ -modules = ["nodejs-24"] +modules = ["nodejs-24", "python-3.11"] [deployment] router = "application" diff --git a/artifacts/ehsan-poc/index.html b/artifacts/ehsan-poc/index.html index 22123dd..359e252 100644 --- a/artifacts/ehsan-poc/index.html +++ b/artifacts/ehsan-poc/index.html @@ -15,7 +15,7 @@ - +
diff --git a/artifacts/ehsan-poc/src/assets/riyal.png b/artifacts/ehsan-poc/src/assets/riyal.png new file mode 100644 index 0000000..c5b951a Binary files /dev/null and b/artifacts/ehsan-poc/src/assets/riyal.png differ diff --git a/artifacts/ehsan-poc/src/components/OpportunityCard.tsx b/artifacts/ehsan-poc/src/components/OpportunityCard.tsx index 69a2d2e..e8f4569 100644 --- a/artifacts/ehsan-poc/src/components/OpportunityCard.tsx +++ b/artifacts/ehsan-poc/src/components/OpportunityCard.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Share2, ShoppingCart } from "lucide-react"; import { getNeedImage } from "../lib/needImages"; +import { Riyal } from "@/components/Riyal"; interface OpportunityCardProps { request: { @@ -17,8 +18,6 @@ interface OpportunityCardProps { }; } -const RIYAL = "﷼"; - export function OpportunityCard({ request }: OpportunityCardProps) { const { t } = useLanguage(); const [, setLocation] = useLocation(); @@ -78,14 +77,14 @@ export function OpportunityCard({ request }: OpportunityCardProps) {

{t.opportunities.remainingShort}

-

- {RIYAL} {remaining.toLocaleString()} +

+ {remaining.toLocaleString()}

{t.opportunities.collectedShort}

-

- {RIYAL} {request.collectedAmount.toLocaleString()} +

+ {request.collectedAmount.toLocaleString()}

@@ -113,7 +112,7 @@ export function OpportunityCard({ request }: OpportunityCardProps) {
- {RIYAL} + + ); +} diff --git a/artifacts/ehsan-poc/src/index.css b/artifacts/ehsan-poc/src/index.css index 4d2d9e3..93fd74e 100644 --- a/artifacts/ehsan-poc/src/index.css +++ b/artifacts/ehsan-poc/src/index.css @@ -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 "tw-animate-css"; @plugin "@tailwindcss/typography"; @@ -126,7 +126,7 @@ --chart-4: 143 20% 80%; --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-mono: Menlo, monospace; --radius: 0.5rem; diff --git a/artifacts/ehsan-poc/src/lib/i18n/translations.ts b/artifacts/ehsan-poc/src/lib/i18n/translations.ts index d3fb9d7..6411713 100644 --- a/artifacts/ehsan-poc/src/lib/i18n/translations.ts +++ b/artifacts/ehsan-poc/src/lib/i18n/translations.ts @@ -62,6 +62,8 @@ export const en = { searchOpportunities: "Search Donation Opportunities", searchLabel: "Find a cause to support", searchButton: "Search", + heroBadge: "National Platform for Charitable Work", + heroBrowse: "Browse Opportunities", featuredTitle: "Featured Opportunities", noResults: "No opportunities match your search.", }, @@ -270,6 +272,8 @@ export const ar = { searchOpportunities: "ابحث في فرص التبرع", searchLabel: "ابحث عن قضية لدعمها", searchButton: "بحث", + heroBadge: "المنصة الوطنية للعمل الخيري", + heroBrowse: "تصفّح الفرص", featuredTitle: "الفرص المميزة", noResults: "لا توجد فرص تطابق بحثك.", }, diff --git a/artifacts/ehsan-poc/src/pages/admin.tsx b/artifacts/ehsan-poc/src/pages/admin.tsx index 4875cac..778614b 100644 --- a/artifacts/ehsan-poc/src/pages/admin.tsx +++ b/artifacts/ehsan-poc/src/pages/admin.tsx @@ -17,6 +17,7 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Link } from "wouter"; +import { Riyal } from "@/components/Riyal"; export default function Admin() { const { t } = useLanguage(); @@ -70,7 +71,7 @@ export default function Admin() { {t.needTypes[req.needType as keyof typeof t.needTypes] || req.needType} - {req.requestedAmount.toLocaleString()} ﷼ + {req.requestedAmount.toLocaleString()} {t.statuses[req.status as keyof typeof t.statuses] || req.status} diff --git a/artifacts/ehsan-poc/src/pages/donate.tsx b/artifacts/ehsan-poc/src/pages/donate.tsx index 3655aae..1e1e947 100644 --- a/artifacts/ehsan-poc/src/pages/donate.tsx +++ b/artifacts/ehsan-poc/src/pages/donate.tsx @@ -17,8 +17,8 @@ import { Checkbox } from "@/components/ui/checkbox"; import { CheckCircle, Heart, Gift, ShoppingCart, Check } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; import { getNeedImage } from "../lib/needImages"; +import { Riyal } from "@/components/Riyal"; -const RIYAL = "﷼"; const PRESETS = [100, 50, 10]; const schema = z.object({ @@ -203,11 +203,11 @@ export default function Donate() {

{t.opportunities.remainingShort}

-

{RIYAL} {remaining.toLocaleString()}

+

{remaining.toLocaleString()}

{t.opportunities.collectedShort}

-

{RIYAL} {request.collectedAmount.toLocaleString()}

+

{request.collectedAmount.toLocaleString()}

@@ -244,7 +244,7 @@ export default function Donate() { }`} data-testid={`preset-${p}`} > - {RIYAL} {p} + {p} ))}
@@ -252,7 +252,7 @@ export default function Donate() { {/* Custom amount */}
- {RIYAL} +
{t.donate.amount} - {RIYAL} {Number(amount).toLocaleString()} + {Number(amount).toLocaleString()}
diff --git a/artifacts/ehsan-poc/src/pages/home.tsx b/artifacts/ehsan-poc/src/pages/home.tsx index fb5910b..284d4fc 100644 --- a/artifacts/ehsan-poc/src/pages/home.tsx +++ b/artifacts/ehsan-poc/src/pages/home.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { OpportunityCard } from "../components/OpportunityCard"; +import { Riyal } from "@/components/Riyal"; import { Link } from "wouter"; import { Search } from "lucide-react"; @@ -26,129 +27,183 @@ export default function Home() { }); return ( -
- {/* Hero */} -
-

- {t.home.heroTitle} -

-

- {t.home.heroSubtitle} -

+ <> + {/* Hero — official EHSAN green banner */} +
+
+ {/* decorative leaves (visual only) */} + + - {/* Search Bar */} -
- -
-
- - setQuery(e.target.value)} - data-testid="input-search" - /> +
+
+ + {t.home.heroBadge} + +

+ {t.home.heroTitle} +

+

+ {t.home.heroSubtitle} +

+
+ + + + + +
-
- {/* Stats */} - {statsLoading ? ( -
- - - -
- ) : ( -
- - - - {t.home.totalRequests} - - - -
- {stats?.totalRequests || 0} -
-
-
- - - - {t.home.totalCollected} - - - -
- {stats?.totalCollected?.toLocaleString() || 0} ﷼ -
-
-
- - - - {t.home.totalClosed} - - - -
- {stats?.totalClosed || 0} -
-
-
-
- )} - - {/* Featured Opportunities */} -
-
-

{t.home.featuredTitle}

- - - -
- - {pubLoading ? ( -
- {[1, 2, 3].map((i) => )} -
- ) : filtered.length === 0 ? ( -
- {query.trim() ? t.home.noResults : t.opportunities.noOpportunities} +
+ {/* Stats */} + {statsLoading ? ( +
+ + +
) : ( -
- {filtered.slice(0, 6).map((request) => ( - - ))} -
- )} -
- - {/* Workflow Steps */} -
-

{t.home.workflowTitle}

-
- {Array.from({ length: 10 }).map((_, i) => ( - - -
- {i + 1} -
-
- {t.workflow[`step${i + 1}` as keyof typeof t.workflow]} +
+ + + + {t.home.totalRequests} + + + +
+ {stats?.totalRequests || 0}
- ))} -
-
-
+ + + + {t.home.totalCollected} + + + +
+ {stats?.totalCollected?.toLocaleString() || 0} +
+
+
+ + + + {t.home.totalClosed} + + + +
+ {stats?.totalClosed || 0} +
+
+
+
+ )} + + {/* Featured Opportunities */} +
+
+

{t.home.featuredTitle}

+ + + +
+ + {/* Search */} +
+ +
+
+ + setQuery(e.target.value)} + data-testid="input-search" + /> +
+ +
+
+ + {pubLoading ? ( +
+ {[1, 2, 3].map((i) => )} +
+ ) : filtered.length === 0 ? ( +
+ {query.trim() ? t.home.noResults : t.opportunities.noOpportunities} +
+ ) : ( +
+ {filtered.slice(0, 6).map((request) => ( + + ))} +
+ )} +
+ + {/* Workflow Steps */} +
+

{t.home.workflowTitle}

+
+ {Array.from({ length: 10 }).map((_, i) => ( + + +
+ {i + 1} +
+
+ {t.workflow[`step${i + 1}` as keyof typeof t.workflow]} +
+
+
+ ))} +
+
+ + ); } diff --git a/artifacts/ehsan-poc/src/pages/request.tsx b/artifacts/ehsan-poc/src/pages/request.tsx index 811faf1..5a17630 100644 --- a/artifacts/ehsan-poc/src/pages/request.tsx +++ b/artifacts/ehsan-poc/src/pages/request.tsx @@ -12,6 +12,7 @@ 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), @@ -227,7 +228,7 @@ export default function RequestSupport() { name="requestedAmount" render={({ field }) => ( - {t.request.amount} (﷼) + {t.request.amount} () diff --git a/artifacts/ehsan-poc/src/pages/track.tsx b/artifacts/ehsan-poc/src/pages/track.tsx index c978ba1..9e1c123 100644 --- a/artifacts/ehsan-poc/src/pages/track.tsx +++ b/artifacts/ehsan-poc/src/pages/track.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Check, Clock } from "lucide-react"; import { Link } from "wouter"; +import { Riyal } from "@/components/Riyal"; const STEPS = [ "step1", "step2", "step3", "step4", "step5", @@ -100,13 +101,13 @@ export default function Track() {

{t.request.amount}

-

{request.requestedAmount.toLocaleString()} ﷼

+

{request.requestedAmount.toLocaleString()}

{t.opportunities.collected}

-

{request.collectedAmount.toLocaleString()} ﷼

+

{request.collectedAmount.toLocaleString()}

{request.donorName && (
diff --git a/attached_assets/image_1780681611047.png b/attached_assets/image_1780681611047.png new file mode 100644 index 0000000..e66c49b Binary files /dev/null and b/attached_assets/image_1780681611047.png differ diff --git a/attached_assets/image_1780681631794.png b/attached_assets/image_1780681631794.png new file mode 100644 index 0000000..327e23f Binary files /dev/null and b/attached_assets/image_1780681631794.png differ diff --git a/attached_assets/image_1780681678157.png b/attached_assets/image_1780681678157.png new file mode 100644 index 0000000..342e177 Binary files /dev/null and b/attached_assets/image_1780681678157.png differ