diff --git a/features/platform-analytics/frontend-platform/src/hooks/index.ts b/features/platform-analytics/frontend-platform/src/hooks/index.ts index 663fc9908..8e810a272 100644 --- a/features/platform-analytics/frontend-platform/src/hooks/index.ts +++ b/features/platform-analytics/frontend-platform/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useAdminQuery'; export * from './useAnalyticsQuery'; +export * from './useSeoQuery'; diff --git a/features/platform-analytics/frontend-platform/src/hooks/useSeoQuery.ts b/features/platform-analytics/frontend-platform/src/hooks/useSeoQuery.ts new file mode 100644 index 000000000..4d65ddfa7 --- /dev/null +++ b/features/platform-analytics/frontend-platform/src/hooks/useSeoQuery.ts @@ -0,0 +1,178 @@ +/** + * SEO Query Hooks + * + * Typed wrappers around @tanstack/react-query for the SEO analytics page. + * Endpoints map to the SeoController routes under `insights/seo/`. + */ + +import { useQuery, type UseQueryResult } from '@tanstack/react-query'; + +import { useAnalyticsFilters } from '../context/AnalyticsFilterContext'; +import type { + SeoOverviewKPIs, + SeoLandingPagesResponse, + SeoCampaignsResponse, + SeoRankingsResponse, + SeoKeywordsResponse, + SeoPositionTrendPoint, + SeoCrawlCoverageResponse, +} from '../types/analytics-api'; + +// ============================================================================ +// Stale Time +// ============================================================================ + +const STALE_TIME = 300_000; // 5 minutes — SEO data changes slowly + +// ============================================================================ +// Base Fetch Helper +// ============================================================================ + +function buildUrl(endpoint: string, params?: Record): string { + const base = `/api/analytics/insights/seo/${endpoint}`; + if (!params || Object.keys(params).length === 0) return base; + const search = new URLSearchParams(params).toString(); + return `${base}?${search}`; +} + +async function fetchSeo(endpoint: string, params?: Record): Promise { + const url = buildUrl(endpoint, params); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`SEO API error: ${response.status} ${response.statusText}`); + } + return response.json(); +} + +// ============================================================================ +// Overview Hook +// ============================================================================ + +export function useSeoOverview(): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + return useQuery({ + queryKey: ['analytics', 'seo', 'overview', filterParams], + queryFn: () => fetchSeo('overview', filterParams), + staleTime: STALE_TIME, + }); +} + +// ============================================================================ +// Landing Pages Hook +// ============================================================================ + +export function useSeoLandingPages(options?: { + domain?: string; + sort?: 'views' | 'time' | 'bounce'; + limit?: number; +}): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + const params: Record = { ...filterParams }; + if (options?.domain) params.domain = options.domain; + if (options?.sort) params.sort = options.sort; + if (options?.limit) params.limit = String(options.limit); + + return useQuery({ + queryKey: ['analytics', 'seo', 'landing-pages', params], + queryFn: () => fetchSeo('landing-pages', params), + staleTime: STALE_TIME, + }); +} + +// ============================================================================ +// Campaigns Hook +// ============================================================================ + +export function useSeoCampaigns(campaignId?: string): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + const params: Record = { ...filterParams }; + if (campaignId) params.campaignId = campaignId; + + return useQuery({ + queryKey: ['analytics', 'seo', 'campaigns', params], + queryFn: () => fetchSeo('campaigns', params), + staleTime: STALE_TIME, + }); +} + +// ============================================================================ +// Rankings Hook (GSC data — task #7 endpoint) +// ============================================================================ + +export function useSeoRankings(options?: { + path?: string; + domain?: string; + limit?: number; +}): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + const params: Record = { ...filterParams }; + if (options?.path) params.path = options.path; + if (options?.domain) params.domain = options.domain; + if (options?.limit) params.limit = String(options.limit); + + return useQuery({ + queryKey: ['analytics', 'seo', 'rankings', params], + queryFn: () => fetchSeo('rankings', params), + staleTime: STALE_TIME, + }); +} + +// ============================================================================ +// Keywords Hook (GSC data — task #7 endpoint) +// ============================================================================ + +export function useSeoKeywords(options?: { + path?: string; + domain?: string; + limit?: number; +}): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + const params: Record = { ...filterParams }; + if (options?.path) params.path = options.path; + if (options?.domain) params.domain = options.domain; + if (options?.limit) params.limit = String(options.limit); + + return useQuery({ + queryKey: ['analytics', 'seo', 'keywords', params], + queryFn: () => fetchSeo('keywords', params), + staleTime: STALE_TIME, + }); +} + +// ============================================================================ +// Position Trend Hook (for selected page drill-down) +// ============================================================================ + +export function useSeoPositionTrend( + path: string, + options?: { domain?: string }, +): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + const params: Record = { ...filterParams, path }; + if (options?.domain) params.domain = options.domain; + + return useQuery({ + queryKey: ['analytics', 'seo', 'position-trend', params], + queryFn: () => fetchSeo('rankings/trend', params), + staleTime: STALE_TIME, + enabled: path.length > 0, + }); +} + +// ============================================================================ +// Crawl Coverage Hook +// ============================================================================ + +export function useSeoCrawlCoverage(options?: { + campaignId?: string; +}): UseQueryResult { + const { filterParams } = useAnalyticsFilters(); + const params: Record = { ...filterParams }; + if (options?.campaignId) params.campaignId = options.campaignId; + + return useQuery({ + queryKey: ['analytics', 'seo', 'crawl-coverage', params], + queryFn: () => fetchSeo('crawl-coverage', params), + staleTime: STALE_TIME, + }); +}