Replace home hero with animated EHSAN banner carousel

Task #6: On artifacts/ehsan-poc home page, replaced the green text hero
with an auto-rotating, looping carousel of the four official ehsan.sa
banner images (Zakat, recurring/dawri, Waqf, gift) imported via @assets.

- Auto-advances every 5s, loops, and respects prefers-reduced-motion.
- Clickable dot indicators plus an accessible pause/play toggle (WCAG
  2.2.2) on a translucent backdrop pill.
- Bottom gradient scrim keeps the white CTA + controls readable on all
  four banner backgrounds.
- Removed the overlay heading, subtitle, and the "طلب دعم" button per the
  user; kept the "تصفّح الفرص" CTA linking to /opportunities.
- Added common.pause / common.play i18n keys (AR + EN) for the toggle.

Banners use alt="" (decorative) since the functional CTA is separately
labeled. Verified with tsc --noEmit and home-page screenshots (RTL).
This commit is contained in:
Replit Agent
2026-06-05 18:20:34 +00:00
parent 2828a816ce
commit d371d63c19
2 changed files with 82 additions and 59 deletions
@@ -28,6 +28,8 @@ export const en = {
login: "Login",
logout: "Logout",
cart: "Cart",
pause: "Pause slideshow",
play: "Play slideshow",
},
nav: {
home: "Home",
@@ -238,6 +240,8 @@ export const ar = {
login: "تسجيل الدخول",
logout: "تسجيل الخروج",
cart: "السلة",
pause: "إيقاف العرض",
play: "تشغيل العرض",
},
nav: {
home: "الرئيسية",
+78 -59
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useLanguage } from "../contexts/LanguageContext";
import { useGetStats, useListPublishedRequests } from "@workspace/api-client-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -8,13 +8,30 @@ 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";
import { Search, Pause, Play } from "lucide-react";
import zakatBanner from "@assets/zakatbanarWEB_1780682994527.png";
import dawriBanner from "@assets/dawribanarWEB_1780682998494.png";
import waqfBanner from "@assets/waqfbanerWEB_1780683002610.png";
import giftBanner from "@assets/giftbanarWEB_1780683006569.png";
const HERO_BANNERS = [zakatBanner, dawriBanner, waqfBanner, giftBanner];
export default function Home() {
const { t } = useLanguage();
const { data: stats, isLoading: statsLoading } = useGetStats();
const { data: published, isLoading: pubLoading } = useListPublishedRequests();
const [query, setQuery] = useState("");
const [slide, setSlide] = useState(0);
const [paused, setPaused] = useState(false);
useEffect(() => {
const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduce || paused) return;
const id = setInterval(() => {
setSlide((s) => (s + 1) % HERO_BANNERS.length);
}, 5000);
return () => clearInterval(id);
}, [paused]);
const filtered = (published || []).filter((r) => {
if (!query.trim()) return true;
@@ -28,64 +45,66 @@ export default function Home() {
return (
<>
{/* Hero — official EHSAN green banner */}
<section className="relative overflow-hidden bg-primary text-primary-foreground">
<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>
{/* Hero — auto-rotating banner carousel */}
<section
className="relative w-full overflow-hidden bg-muted"
aria-roledescription="carousel"
aria-label={t.home.heroBrowse}
>
<div className="relative h-[220px] sm:h-[320px] md:h-[420px] lg:h-[480px]">
{HERO_BANNERS.map((src, i) => (
<img
key={i}
src={src}
alt=""
aria-hidden="true"
loading={i === 0 ? "eager" : "lazy"}
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-700 ease-in-out ${
i === slide ? "opacity-100" : "opacity-0"
}`}
/>
))}
<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}
</h1>
<p className="text-lg md:text-xl text-primary-foreground/90 mb-8 leading-relaxed">
{t.home.heroSubtitle}
</p>
<div className="flex flex-wrap gap-3">
<Link href="/opportunities">
<Button
size="lg"
className="bg-white text-primary hover:bg-white/90 font-bold px-7"
data-testid="button-heroBrowse"
>
{t.home.heroBrowse}
</Button>
</Link>
<Link href="/request">
<Button
size="lg"
variant="outline"
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>
{/* bottom scrim keeps the CTA + dots readable over any banner */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-black/45 to-transparent" />
<div className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-4 p-4 md:p-8">
<Link href="/opportunities" className="pointer-events-auto">
<Button
size="lg"
className="bg-white text-primary hover:bg-white/90 font-bold px-7 shadow-lg"
data-testid="button-heroBrowse"
>
{t.home.heroBrowse}
</Button>
</Link>
<div className="pointer-events-auto flex items-center gap-3 rounded-full bg-black/25 px-3 py-2 backdrop-blur-sm">
<button
type="button"
onClick={() => setPaused((p) => !p)}
aria-label={paused ? t.common.play : t.common.pause}
aria-pressed={paused}
data-testid="button-hero-toggle"
className="flex h-5 w-5 items-center justify-center text-white/90 transition-colors hover:text-white"
>
{paused ? <Play className="h-3.5 w-3.5" /> : <Pause className="h-3.5 w-3.5" />}
</button>
<div className="flex items-center gap-2">
{HERO_BANNERS.map((_, i) => (
<button
key={i}
type="button"
onClick={() => setSlide(i)}
aria-label={`${t.home.heroBrowse} ${i + 1}`}
aria-current={i === slide}
data-testid={`dot-hero-${i}`}
className={`h-2.5 rounded-full transition-all duration-300 ${
i === slide ? "w-7 bg-white" : "w-2.5 bg-white/50 hover:bg-white/80"
}`}
/>
))}
</div>
</div>
</div>
</div>