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:
@@ -28,6 +28,8 @@ export const en = {
|
|||||||
login: "Login",
|
login: "Login",
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
cart: "Cart",
|
cart: "Cart",
|
||||||
|
pause: "Pause slideshow",
|
||||||
|
play: "Play slideshow",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: "Home",
|
home: "Home",
|
||||||
@@ -238,6 +240,8 @@ export const ar = {
|
|||||||
login: "تسجيل الدخول",
|
login: "تسجيل الدخول",
|
||||||
logout: "تسجيل الخروج",
|
logout: "تسجيل الخروج",
|
||||||
cart: "السلة",
|
cart: "السلة",
|
||||||
|
pause: "إيقاف العرض",
|
||||||
|
play: "تشغيل العرض",
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: "الرئيسية",
|
home: "الرئيسية",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLanguage } from "../contexts/LanguageContext";
|
import { useLanguage } from "../contexts/LanguageContext";
|
||||||
import { useGetStats, useListPublishedRequests } from "@workspace/api-client-react";
|
import { useGetStats, useListPublishedRequests } from "@workspace/api-client-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 { OpportunityCard } from "../components/OpportunityCard";
|
||||||
import { Riyal } from "@/components/Riyal";
|
import { Riyal } from "@/components/Riyal";
|
||||||
import { Link } from "wouter";
|
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() {
|
export default function Home() {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { data: stats, isLoading: statsLoading } = useGetStats();
|
const { data: stats, isLoading: statsLoading } = useGetStats();
|
||||||
const { data: published, isLoading: pubLoading } = useListPublishedRequests();
|
const { data: published, isLoading: pubLoading } = useListPublishedRequests();
|
||||||
const [query, setQuery] = useState("");
|
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) => {
|
const filtered = (published || []).filter((r) => {
|
||||||
if (!query.trim()) return true;
|
if (!query.trim()) return true;
|
||||||
@@ -28,64 +45,66 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hero — official EHSAN green banner */}
|
{/* Hero — auto-rotating banner carousel */}
|
||||||
<section className="relative overflow-hidden bg-primary text-primary-foreground">
|
<section
|
||||||
<div
|
className="relative w-full overflow-hidden bg-muted"
|
||||||
className="absolute inset-0 opacity-25"
|
aria-roledescription="carousel"
|
||||||
style={{
|
aria-label={t.home.heroBrowse}
|
||||||
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%)",
|
<div className="relative h-[220px] sm:h-[320px] md:h-[420px] lg:h-[480px]">
|
||||||
}}
|
{HERO_BANNERS.map((src, i) => (
|
||||||
/>
|
<img
|
||||||
{/* decorative leaves (visual only) */}
|
key={i}
|
||||||
<svg
|
src={src}
|
||||||
className="pointer-events-none absolute -bottom-10 start-0 h-56 w-56 text-white/10"
|
alt=""
|
||||||
viewBox="0 0 200 200"
|
aria-hidden="true"
|
||||||
fill="currentColor"
|
loading={i === 0 ? "eager" : "lazy"}
|
||||||
aria-hidden="true"
|
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-700 ease-in-out ${
|
||||||
>
|
i === slide ? "opacity-100" : "opacity-0"
|
||||||
<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">
|
{/* bottom scrim keeps the CTA + dots readable over any banner */}
|
||||||
<div className="max-w-2xl">
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-black/45 to-transparent" />
|
||||||
<span className="inline-block rounded-full bg-white/15 px-4 py-1 text-sm font-medium mb-5">
|
|
||||||
{t.home.heroBadge}
|
<div className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-4 p-4 md:p-8">
|
||||||
</span>
|
<Link href="/opportunities" className="pointer-events-auto">
|
||||||
<h1 className="text-3xl md:text-5xl font-bold leading-tight mb-5">
|
<Button
|
||||||
{t.home.heroTitle}
|
size="lg"
|
||||||
</h1>
|
className="bg-white text-primary hover:bg-white/90 font-bold px-7 shadow-lg"
|
||||||
<p className="text-lg md:text-xl text-primary-foreground/90 mb-8 leading-relaxed">
|
data-testid="button-heroBrowse"
|
||||||
{t.home.heroSubtitle}
|
>
|
||||||
</p>
|
{t.home.heroBrowse}
|
||||||
<div className="flex flex-wrap gap-3">
|
</Button>
|
||||||
<Link href="/opportunities">
|
</Link>
|
||||||
<Button
|
|
||||||
size="lg"
|
<div className="pointer-events-auto flex items-center gap-3 rounded-full bg-black/25 px-3 py-2 backdrop-blur-sm">
|
||||||
className="bg-white text-primary hover:bg-white/90 font-bold px-7"
|
<button
|
||||||
data-testid="button-heroBrowse"
|
type="button"
|
||||||
>
|
onClick={() => setPaused((p) => !p)}
|
||||||
{t.home.heroBrowse}
|
aria-label={paused ? t.common.play : t.common.pause}
|
||||||
</Button>
|
aria-pressed={paused}
|
||||||
</Link>
|
data-testid="button-hero-toggle"
|
||||||
<Link href="/request">
|
className="flex h-5 w-5 items-center justify-center text-white/90 transition-colors hover:text-white"
|
||||||
<Button
|
>
|
||||||
size="lg"
|
{paused ? <Play className="h-3.5 w-3.5" /> : <Pause className="h-3.5 w-3.5" />}
|
||||||
variant="outline"
|
</button>
|
||||||
className="border-white/70 bg-transparent text-white hover:bg-white/10 font-bold px-7"
|
<div className="flex items-center gap-2">
|
||||||
data-testid="button-heroRequest"
|
{HERO_BANNERS.map((_, i) => (
|
||||||
>
|
<button
|
||||||
{t.nav.requestSupport}
|
key={i}
|
||||||
</Button>
|
type="button"
|
||||||
</Link>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user