"use client"; import { cn } from "@/lib/utils"; import gsap from "gsap"; import { useLayoutEffect, useRef } from "react"; import { AnimatedCounter } from "./animated-counter"; import { SparkleIcon } from "./icons"; import { LiveClock } from "./live-clock"; type Props = { total: number; active: number; closingSoon: number; isLoading: boolean; }; function greeting() { const h = new Date().getHours(); if (h < 5) return "Working late"; if (h < 12) return "Good morning"; if (h < 17) return "Good afternoon"; if (h < 21) return "Good evening"; return "Good night"; } export function DashboardHero({ total, active, closingSoon, isLoading, }: Props) { const rootRef = useRef(null); const auroraRef = useRef(null); const orbARef = useRef(null); const orbBRef = useRef(null); const orbCRef = useRef(null); const titleRef = useRef(null); const shineRef = useRef(null); useLayoutEffect(() => { const reduced = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const resetIntroTargets = () => { const root = rootRef.current; if (root) { gsap.killTweensOf(root); gsap.set(root, { clearProps: "opacity,transform" }); } const title = titleRef.current; if (title) { const inner = title.querySelector("[data-title-inner]"); if (inner) { gsap.killTweensOf(inner); gsap.set(inner, { clearProps: "opacity,transform" }); } } root?.querySelectorAll("[data-hero-pill]").forEach((el) => { gsap.killTweensOf(el); gsap.set(el, { clearProps: "opacity,transform,scale" }); }); }; const ctx = gsap.context(() => { if (reduced) return; const root = rootRef.current; if (root) { // Do not tween root opacity: Strict Mode + revert can leave the whole hero // at opacity 0. Motion uses translateY only. gsap.fromTo( root, { y: 28 }, { y: 0, duration: 0.8, ease: "power3.out", }, ); } // Animate a wrapper around the headline, not per-glyph. Transforms on // children inside `bg-clip-text` / `text-transparent` often paint as fully // invisible in WebKit and some Chromium builds. const titleInner = titleRef.current?.querySelector( "[data-title-inner]", ); if (titleInner) { gsap.fromTo( titleInner, { y: 36 }, { y: 0, duration: 0.75, ease: "back.out(1.7)", delay: 0.2, }, ); } const pills = rootRef.current?.querySelectorAll("[data-hero-pill]"); if (pills?.length) { gsap.fromTo( pills, { y: 16, scale: 0.92 }, { y: 0, scale: 1, duration: 0.55, stagger: 0.08, delay: 0.55, ease: "back.out(1.6)", }, ); } if (auroraRef.current) { gsap.to(auroraRef.current, { rotate: 360, duration: 42, ease: "none", repeat: -1, }); } if (orbARef.current) { gsap.to(orbARef.current, { x: 42, y: -26, scale: 1.18, duration: 7, repeat: -1, yoyo: true, ease: "sine.inOut", }); } if (orbBRef.current) { gsap.to(orbBRef.current, { x: -32, y: 24, scale: 1.12, duration: 8, repeat: -1, yoyo: true, ease: "sine.inOut", }); } if (orbCRef.current) { gsap.to(orbCRef.current, { x: 22, y: 18, scale: 1.08, duration: 9.5, repeat: -1, yoyo: true, ease: "sine.inOut", }); } if (shineRef.current) { const shineTl = gsap.timeline({ repeat: -1, repeatDelay: 3.5 }); shineTl .set(shineRef.current, { xPercent: -160, opacity: 0 }) .to(shineRef.current, { opacity: 1, duration: 0.2, delay: 1.8, }) .to( shineRef.current, { xPercent: 260, duration: 1.8, ease: "power2.inOut", }, "<", ) .to(shineRef.current, { opacity: 0, duration: 0.2 }); } if (!root) return; const onMove = (e: MouseEvent) => { const r = root.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width - 0.5; const py = (e.clientY - r.top) / r.height - 0.5; gsap.to(orbARef.current, { xPercent: px * 22, yPercent: py * 18, duration: 1.2, ease: "power2.out", }); gsap.to(orbBRef.current, { xPercent: px * -18, yPercent: py * 20, duration: 1.3, ease: "power2.out", }); gsap.to(orbCRef.current, { xPercent: px * 12, yPercent: py * -14, duration: 1.4, ease: "power2.out", }); }; root.addEventListener("mousemove", onMove); return () => root.removeEventListener("mousemove", onMove); }, rootRef); return () => { const root = rootRef.current; const title = titleRef.current; const titleInner = title?.querySelector("[data-title-inner]"); if (titleInner) gsap.killTweensOf(titleInner); if (root) gsap.killTweensOf(root); root ?.querySelectorAll("[data-hero-pill]") .forEach((el) => gsap.killTweensOf(el)); ctx.revert(); // React Strict Mode remounts before intro tweens finish; revert can leave // inline opacity/transform stuck (invisible headline / blank hero). resetIntroTargets(); }; }, []); const headline = `${greeting()}, welcome to OppLens`; const sub = "Real-time pulse on every tender flowing through your pipeline."; return (
Live tender intelligence

{headline}

{sub}

); } function HeroPill({ label, value, suffix, loading, tone, }: { label: string; value: number; suffix?: string; loading: boolean; tone: "primary" | "emerald" | "amber"; }) { const tones: Record = { primary: "bg-gradient-to-br from-white to-slate-100 text-[#1B1F3A] shadow-[0_8px_24px_-10px_rgba(15,23,42,0.2)] ring-1 ring-slate-900/5 dark:from-white/95 dark:to-white/75 dark:shadow-[0_10px_30px_-12px_rgba(255,255,255,0.45)] dark:ring-0", emerald: "border border-emerald-200/80 bg-gradient-to-br from-emerald-50 to-emerald-100/50 text-emerald-900 shadow-[0_8px_24px_-10px_rgba(16,185,129,0.25)] dark:from-emerald-400/30 dark:to-emerald-300/5 dark:text-emerald-50 dark:border-emerald-300/30 dark:shadow-[0_10px_30px_-12px_rgba(16,185,129,0.55)]", amber: "border border-orange-200/80 bg-gradient-to-br from-amber-50 to-orange-50/80 text-amber-950 shadow-[0_8px_24px_-10px_rgba(251,146,60,0.2)] dark:from-orange-400/30 dark:to-amber-300/5 dark:text-orange-50 dark:border-orange-300/30 dark:shadow-[0_10px_30px_-12px_rgba(251,146,60,0.55)]", }; return (
{label} {loading ? ( "—" ) : ( )}
); }