chore(env): update application version to 2.1.2 in production environment

- Incremented the application version to 2.1.2 for production deployment.
- Enhanced the DashboardHero component with additional animation effects and improved layout.
- Refactored LiveClock and StatCard components for better styling and performance.
- Introduced new shimmer animation in NoticeTypeBreakdown for visual enhancement.
- Updated the trends chart with improved stroke effects and hover animations.
This commit is contained in:
AmirReza Jamali
2026-05-18 13:37:44 +03:30
parent f5099d8e04
commit adc5d7d307
9 changed files with 614 additions and 158 deletions
+260 -71
View File
@@ -23,70 +23,204 @@ function greeting() {
return "Good night";
}
export function DashboardHero({ total, active, closingSoon, isLoading }: Props) {
export function DashboardHero({
total,
active,
closingSoon,
isLoading,
}: Props) {
const rootRef = useRef<HTMLDivElement>(null);
const auroraRef = useRef<HTMLDivElement>(null);
const orbARef = useRef<HTMLDivElement>(null);
const orbBRef = useRef<HTMLDivElement>(null);
const orbCRef = useRef<HTMLDivElement>(null);
const titleRef = useRef<HTMLHeadingElement>(null);
const shineRef = useRef<HTMLSpanElement>(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<HTMLElement>("[data-title-inner]");
if (inner) {
gsap.killTweensOf(inner);
gsap.set(inner, { clearProps: "opacity,transform" });
}
}
root?.querySelectorAll<HTMLElement>("[data-hero-pill]").forEach((el) => {
gsap.killTweensOf(el);
gsap.set(el, { clearProps: "opacity,transform,scale" });
});
};
const ctx = gsap.context(() => {
if (reduced) return;
gsap.from(rootRef.current, {
opacity: 0,
y: 24,
duration: 0.7,
ease: "power3.out",
});
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",
},
);
}
const chars = titleRef.current?.querySelectorAll<HTMLElement>("[data-char]");
if (chars && chars.length) {
gsap.from(chars, {
y: 28,
opacity: 0,
rotateX: -50,
duration: 0.6,
stagger: 0.025,
ease: "back.out(1.6)",
delay: 0.15,
// 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<HTMLElement>(
"[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<HTMLElement>("[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,
});
}
gsap.from("[data-hero-pill]", {
y: 12,
opacity: 0,
duration: 0.5,
stagger: 0.08,
delay: 0.4,
ease: "power2.out",
});
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",
});
}
gsap.to(orbARef.current, {
x: 30,
y: -20,
scale: 1.15,
duration: 6,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
});
gsap.to(orbBRef.current, {
x: -25,
y: 18,
scale: 1.1,
duration: 7,
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 () => ctx.revert();
return () => {
const root = rootRef.current;
const title = titleRef.current;
const titleInner = title?.querySelector<HTMLElement>("[data-title-inner]");
if (titleInner) gsap.killTweensOf(titleInner);
if (root) gsap.killTweensOf(root);
root
?.querySelectorAll<HTMLElement>("[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`;
@@ -95,77 +229,120 @@ export function DashboardHero({ total, active, closingSoon, isLoading }: Props)
return (
<div
ref={rootRef}
className="relative overflow-hidden rounded-2xl border border-stroke-dark/60 bg-gradient-to-br from-dark via-dark-2 to-dark-3 p-6 text-white shadow-1 dark:border-stroke-dark dark:from-[#0B121F] dark:via-dark dark:to-dark-2 dark:shadow-card md:p-8"
className="relative isolate overflow-hidden rounded-3xl border border-stroke/70 bg-gradient-to-br from-slate-50 via-white to-violet-50/40 p-6 text-slate-900 shadow-1 md:p-8 lg:p-10 dark:border-transparent dark:bg-[#0A0E1E] dark:bg-none dark:text-white dark:shadow-card"
>
<div
ref={auroraRef}
aria-hidden
className="pointer-events-none absolute -inset-[40%] z-0 opacity-45 dark:opacity-80"
style={{
backgroundImage:
"conic-gradient(from 0deg at 50% 50%, #5750F1 0deg, #0ABEF9 80deg, transparent 140deg, #8155FF 200deg, #5750F1 280deg, transparent 320deg, #5750F1 360deg)",
filter: "blur(64px)",
}}
/>
<div
ref={orbARef}
aria-hidden
className="pointer-events-none absolute -right-16 -top-24 h-72 w-72 rounded-full bg-primary/45 blur-3xl"
className="pointer-events-none absolute -right-24 -top-32 z-0 h-80 w-80 rounded-full bg-[#5750F1]/25 blur-3xl dark:bg-[#5750F1]/55"
/>
<div
ref={orbBRef}
aria-hidden
className="pointer-events-none absolute -bottom-24 -left-10 h-64 w-64 rounded-full bg-blue/35 blur-3xl"
className="pointer-events-none absolute -bottom-32 -left-16 z-0 h-72 w-72 rounded-full bg-[#0ABEF9]/20 blur-3xl dark:bg-[#0ABEF9]/45"
/>
<div
ref={orbCRef}
aria-hidden
className="pointer-events-none absolute left-1/2 top-1/3 z-0 h-56 w-56 -translate-x-1/2 rounded-full bg-[#F23030]/10 blur-3xl dark:bg-[#F23030]/20"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 opacity-[0.06] dark:hidden"
style={{
backgroundImage:
"radial-gradient(circle at 1px 1px, rgb(15 23 42) 1px, transparent 0)",
backgroundSize: "20px 20px",
}}
/>
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.07]"
className="pointer-events-none absolute inset-0 z-0 hidden opacity-[0.07] dark:block"
style={{
backgroundImage:
"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",
backgroundSize: "18px 18px",
backgroundSize: "20px 20px",
}}
/>
<div className="relative z-10 flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 rounded-3xl ring-1 ring-inset ring-slate-900/[0.06] dark:ring-white/10"
/>
<div className="relative z-10 flex flex-col gap-7 md:flex-row md:items-end md:justify-between">
<div className="flex-1">
<span
data-hero-pill
className="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-medium uppercase tracking-wider backdrop-blur-sm"
className="inline-flex items-center gap-2 rounded-full border border-slate-200/90 bg-white/90 px-3.5 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-600 backdrop-blur-xl dark:border-white/15 dark:bg-white/[0.06] dark:text-white/85"
>
<SparkleIcon className="size-3.5" />
<span className="relative flex size-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-1.5 rounded-full bg-emerald-400" />
</span>
Live tender intelligence
<SparkleIcon className="size-3.5 text-slate-500 dark:text-white/80" />
</span>
<h1
ref={titleRef}
className="mt-4 text-3xl font-bold leading-tight md:text-[40px] md:leading-[1.1]"
className="relative z-[1] mt-5 max-w-2xl text-3xl font-bold leading-tight md:text-[44px] md:leading-[1.05]"
aria-label={headline}
>
{Array.from(headline).map((ch, i) => (
<span
key={i}
data-char
className="inline-block whitespace-pre will-change-transform"
>
{ch}
<span
data-title-inner
className="relative z-[1] inline-block will-change-transform"
>
<span className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-500 bg-clip-text text-transparent dark:from-white dark:via-white dark:to-white/60">
{headline}
</span>
))}
</span>
<span
aria-hidden
className="pointer-events-none absolute inset-0 z-0 overflow-hidden"
>
<span
ref={shineRef}
className="absolute inset-y-0 block w-1/3 opacity-0 blur-[10px] [background:linear-gradient(100deg,transparent_0%,rgba(87,80,241,0.35)_50%,transparent_100%)] dark:[background:linear-gradient(100deg,transparent_0%,rgba(255,255,255,0.45)_50%,transparent_100%)]"
/>
</span>
</h1>
<p className="mt-3 max-w-xl text-sm text-white/75 md:text-base">
<p className="mt-3 max-w-xl text-sm text-slate-600 md:text-base dark:text-white/65">
{sub}
</p>
<div className="mt-6 flex flex-wrap items-center gap-3">
<div className="mt-7 flex flex-wrap items-center gap-3">
<HeroPill
label="Tracking"
value={total}
suffix=" tenders"
loading={isLoading}
accent="bg-white text-[#3C50E0]"
tone="primary"
/>
<HeroPill
label="Active"
value={active}
loading={isLoading}
accent="bg-emerald-400/20 text-emerald-50 border border-emerald-300/30"
tone="emerald"
/>
<HeroPill
label="Closing soon"
value={closingSoon}
loading={isLoading}
accent="bg-orange-400/20 text-orange-50 border border-orange-300/30"
tone="amber"
/>
</div>
</div>
@@ -181,23 +358,31 @@ function HeroPill({
value,
suffix,
loading,
accent,
tone,
}: {
label: string;
value: number;
suffix?: string;
loading: boolean;
accent: string;
tone: "primary" | "emerald" | "amber";
}) {
const tones: Record<typeof tone, string> = {
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 (
<div
data-hero-pill
className={cn(
"flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-semibold shadow-sm backdrop-blur-md",
accent,
"group relative flex items-center gap-2 overflow-hidden rounded-full px-4 py-2 text-sm font-semibold backdrop-blur-xl transition-transform hover:scale-[1.04]",
tones[tone],
)}
>
<span className="text-xs font-medium uppercase tracking-wider opacity-80">
<span className="text-[10px] font-bold uppercase tracking-[0.16em] opacity-70">
{label}
</span>
<span className="tabular-nums">
@@ -207,6 +392,10 @@ function HeroPill({
<AnimatedCounter value={value} suffix={suffix ?? ""} />
)}
</span>
<span
aria-hidden
className="pointer-events-none absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-700 group-hover:translate-x-full"
/>
</div>
);
}