feat(dashboard): refactor home page to utilize TenderDashboard component
- Replaced the previous home page structure with a simplified TenderDashboard component for improved clarity and maintainability. - Updated the .gitignore file to include IDE-specific files, ensuring a cleaner repository. - Enhanced the useTenderListPresenter to support new parsing logic for cpv_codes in search parameters. - Introduced a new utility function, buildQueryString, for better handling of query parameters in API requests.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"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<HTMLDivElement>(null);
|
||||
const orbARef = useRef<HTMLDivElement>(null);
|
||||
const orbBRef = useRef<HTMLDivElement>(null);
|
||||
const titleRef = useRef<HTMLHeadingElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const reduced =
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
if (reduced) return;
|
||||
|
||||
gsap.from(rootRef.current, {
|
||||
opacity: 0,
|
||||
y: 24,
|
||||
duration: 0.7,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
gsap.from("[data-hero-pill]", {
|
||||
y: 12,
|
||||
opacity: 0,
|
||||
duration: 0.5,
|
||||
stagger: 0.08,
|
||||
delay: 0.4,
|
||||
ease: "power2.out",
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
}, rootRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
const headline = `${greeting()}, welcome to OppLens`;
|
||||
const sub = "Real-time pulse on every tender flowing through your pipeline.";
|
||||
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.07]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",
|
||||
backgroundSize: "18px 18px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-6 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"
|
||||
>
|
||||
<SparkleIcon className="size-3.5" />
|
||||
Live tender intelligence
|
||||
</span>
|
||||
|
||||
<h1
|
||||
ref={titleRef}
|
||||
className="mt-4 text-3xl font-bold leading-tight md:text-[40px] md:leading-[1.1]"
|
||||
aria-label={headline}
|
||||
>
|
||||
{Array.from(headline).map((ch, i) => (
|
||||
<span
|
||||
key={i}
|
||||
data-char
|
||||
className="inline-block whitespace-pre will-change-transform"
|
||||
>
|
||||
{ch}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
|
||||
<p className="mt-3 max-w-xl text-sm text-white/75 md:text-base">
|
||||
{sub}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center gap-3">
|
||||
<HeroPill
|
||||
label="Tracking"
|
||||
value={total}
|
||||
suffix=" tenders"
|
||||
loading={isLoading}
|
||||
accent="bg-white text-[#3C50E0]"
|
||||
/>
|
||||
<HeroPill
|
||||
label="Active"
|
||||
value={active}
|
||||
loading={isLoading}
|
||||
accent="bg-emerald-400/20 text-emerald-50 border border-emerald-300/30"
|
||||
/>
|
||||
<HeroPill
|
||||
label="Closing soon"
|
||||
value={closingSoon}
|
||||
loading={isLoading}
|
||||
accent="bg-orange-400/20 text-orange-50 border border-orange-300/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LiveClock />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroPill({
|
||||
label,
|
||||
value,
|
||||
suffix,
|
||||
loading,
|
||||
accent,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
suffix?: string;
|
||||
loading: boolean;
|
||||
accent: string;
|
||||
}) {
|
||||
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,
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-medium uppercase tracking-wider opacity-80">
|
||||
{label}
|
||||
</span>
|
||||
<span className="tabular-nums">
|
||||
{loading ? (
|
||||
"—"
|
||||
) : (
|
||||
<AnimatedCounter value={value} suffix={suffix ?? ""} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user