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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user