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} + + )} +
+ +
    + {isLoading + ? Array.from({ length: 5 }).map((_, i) => ( +
  • +
    +
    +
  • + )) + : portals.map((portal, index) => ( +
  • +

    + + {index + 1} + + + {portal.length > 60 + ? truncateString(portal, 28, 28) + : portal} + +

    +
  • + ))} + {!isLoading && portals.length === 0 && ( +
  • + No portals configured. +
  • + )} +
+
+ ); +} 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";