diff --git a/src/app/(home)/_components/dashboard/index.tsx b/src/app/(home)/_components/dashboard/index.tsx
index ed7d510..7e93f80 100644
--- a/src/app/(home)/_components/dashboard/index.tsx
+++ b/src/app/(home)/_components/dashboard/index.tsx
@@ -4,6 +4,7 @@ import { ClosingSoonCard } from "./closing-soon";
import { CountryDistribution } from "./country-distribution";
import { DashboardHero } from "./hero";
import { NoticeTypeBreakdown } from "./notice-types";
+import { ScrapePortals } from "./portals";
import { RecentTenders } from "./recent-tenders";
import { StatisticsSection } from "./statistics-section";
import { TenderStatCards } from "./stat-cards";
@@ -19,11 +20,11 @@ export function TenderDashboard() {
totalValue,
valueCurrency,
countries,
- noticeTypes,
trend,
recent,
recentIsLoading,
- closingSoonList,
+ portals,
+ portalsIsLoading,
} = useDashboardData();
return (
@@ -63,9 +64,12 @@ export function TenderDashboard() {
*/}
-
diff --git a/src/app/(home)/_components/dashboard/portals.tsx b/src/app/(home)/_components/dashboard/portals.tsx
new file mode 100644
index 0000000..ebfe0aa
--- /dev/null
+++ b/src/app/(home)/_components/dashboard/portals.tsx
@@ -0,0 +1,113 @@
+"use client";
+
+import { TPortal } from "@/lib/api";
+import { truncateString } from "@/utils/shared";
+import gsap from "gsap";
+import { useLayoutEffect, useRef } from "react";
+import { GlobeIcon } from "./icons";
+
+type Props = {
+ portals: TPortal[];
+ isLoading: boolean;
+};
+
+export function ScrapePortals({ portals, isLoading }: Props) {
+ const rootRef = useRef(null);
+
+ useLayoutEffect(() => {
+ const root = rootRef.current;
+ if (!root) return;
+
+ const reduced =
+ typeof window !== "undefined" &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ const ctx = gsap.context(() => {
+ if (reduced) return;
+ gsap.from(root, {
+ opacity: 0,
+ y: 24,
+ duration: 0.6,
+ ease: "power3.out",
+ delay: 0.35,
+ });
+
+ const rows = root.querySelectorAll("[data-portal-row]");
+ if (rows.length > 0) {
+ gsap.from(rows, {
+ x: 24,
+ opacity: 0,
+ duration: 0.5,
+ stagger: 0.06,
+ delay: 0.55,
+ ease: "power2.out",
+ });
+ }
+ }, root);
+ return () => ctx.revert();
+ }, [portals.length, isLoading]);
+
+ return (
+
+
+
+
+
+
+
+
+ Scrape Portals
+
+
+ Tender sources monitored by the pipeline
+
+
+
+ {!isLoading && portals.length > 0 && (
+
+ {portals.length}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/(home)/_components/dashboard/use-dashboard-data.ts b/src/app/(home)/_components/dashboard/use-dashboard-data.ts
index 4bdcf14..1e93eea 100644
--- a/src/app/(home)/_components/dashboard/use-dashboard-data.ts
+++ b/src/app/(home)/_components/dashboard/use-dashboard-data.ts
@@ -8,6 +8,7 @@ import {
useDashboardTrendQuery,
useTendersQuery,
} from "@/hooks/queries";
+import { useGetPortalsQuery } from "@/hooks/queries/usePortalQueries";
import { useMemo } from "react";
import { toLabeledPoints } from "./to-labeled-points";
@@ -19,6 +20,7 @@ export function useDashboardData() {
const countriesQuery = useDashboardCountriesQuery({ limit: 6 });
const noticeTypesQuery = useDashboardNoticeTypesQuery();
const closingSoonQuery = useDashboardClosingSoonQuery({ limit: 5 });
+ const portalsQuery = useGetPortalsQuery();
const {
data: recentData,
@@ -87,6 +89,8 @@ export function useDashboardData() {
const recent = recentData?.data?.tenders ?? [];
const recentIsLoading = recentPending && recent.length === 0;
+ const portals = portalsQuery.data?.data ?? [];
+ const portalsIsLoading = portalsQuery.isPending && portals.length === 0;
return {
isPending,
@@ -104,5 +108,7 @@ export function useDashboardData() {
recent,
recentIsLoading,
closingSoonList,
+ portals,
+ portalsIsLoading,
};
}
diff --git a/src/hooks/queries/usePortalQueries.ts b/src/hooks/queries/usePortalQueries.ts
new file mode 100644
index 0000000..d024620
--- /dev/null
+++ b/src/hooks/queries/usePortalQueries.ts
@@ -0,0 +1,11 @@
+import { API_ENDPOINTS, portalsService } from "@/lib/api";
+import { useQuery } from "@tanstack/react-query";
+import { useMemo } from "react";
+
+export const useGetPortalsQuery = () => {
+ const queryKey = useMemo(() => [API_ENDPOINTS.PORTALS.GET], []);
+ return useQuery({
+ queryKey,
+ queryFn: () => portalsService.getPortals(),
+ });
+};
\ No newline at end of file
diff --git a/src/lib/api/endpoints.ts b/src/lib/api/endpoints.ts
index b9a1979..13e9e4c 100644
--- a/src/lib/api/endpoints.ts
+++ b/src/lib/api/endpoints.ts
@@ -92,4 +92,7 @@ export const API_ENDPOINTS = {
CLOSING_SOON: "dashboard/closing-soon",
RECENT: "dashboard/recent",
},
+ PORTALS: {
+ GET: "ai-pipeline/scrape/portals",
+ },
} as const;
diff --git a/src/lib/api/services/index.ts b/src/lib/api/services/index.ts
index c502c7a..81a6cb2 100644
--- a/src/lib/api/services/index.ts
+++ b/src/lib/api/services/index.ts
@@ -2,6 +2,8 @@ export * from "./companies-service";
export * from "./dashboard-service";
export * from "./file-service";
export * from "./inquiries-service";
+export * from "./portals-service";
export * from "./profile-service";
export * from "./tenders-service";
export * from "./user-services";
+
diff --git a/src/lib/api/services/portals-service.ts b/src/lib/api/services/portals-service.ts
new file mode 100644
index 0000000..0ab5906
--- /dev/null
+++ b/src/lib/api/services/portals-service.ts
@@ -0,0 +1,15 @@
+import api from "../axios";
+import { API_ENDPOINTS } from "../endpoints";
+import { ApiResponse, PortalsListResponseSchema, TPortal } from "../types";
+
+export const portalsService = {
+ getPortals: async (): Promise> => {
+ try {
+ const response = await api.get(API_ENDPOINTS.PORTALS.GET);
+ return PortalsListResponseSchema.parse(response.data);
+ } catch (error) {
+ console.error("ERROR caught in portals service => getPortals", error);
+ throw error;
+ }
+ },
+};
\ No newline at end of file
diff --git a/src/lib/api/types/TPortals.ts b/src/lib/api/types/TPortals.ts
new file mode 100644
index 0000000..db5540a
--- /dev/null
+++ b/src/lib/api/types/TPortals.ts
@@ -0,0 +1,11 @@
+import { z } from "zod";
+import { createApiResponseSchema } from "./Factory";
+
+export const PortalSchema = z.string();
+
+export const PortalsListResponseSchema = createApiResponseSchema(
+ z.array(PortalSchema),
+);
+
+export type TPortal = z.infer;
+export type TPortalsListResponse = z.infer;
diff --git a/src/lib/api/types/index.ts b/src/lib/api/types/index.ts
index f83c1bf..2ceefd4 100644
--- a/src/lib/api/types/index.ts
+++ b/src/lib/api/types/index.ts
@@ -1,14 +1,15 @@
export * from "./CompanyCategories";
export * from "./CompanyDocuments";
export * from "./Customers";
+export * from "./Dashboard";
export * from "./Factory";
+export * from "./Feedback";
export * from "./Posts";
export * from "./Profile";
export * from "./shared";
-export * from "./TCompany";
-export * from "./User";
-export * from "./TInquiries";
export * from "./TCms";
-export * from "./Feedback";
+export * from "./TCompany";
export * from "./Tenders";
-export * from "./Dashboard";
+export * from "./TInquiries";
+export * from "./TPortals";
+export * from "./User";