feat: add Search panel to dashboard and enrich Search tab
Dashboard: compact Search Performance panel showing top 5 queries, clicks, impressions, and avg position alongside Campaigns. Search tab: clicks/impressions trend chart, top query position tracker cards, and new queries badge.
This commit is contained in:
@@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
- **Google Search Console integration.** Connect your Google Search Console account in Settings > Integrations to see which search queries bring visitors to your site. A new "Search" tab on your dashboard shows total clicks, impressions, average CTR, and average ranking position — with percentage changes compared to the previous period. Browse your top search queries and top pages in sortable, paginated tables. Click any query to see which pages rank for it, or click any page to see which queries drive traffic to it. Pulse only requests read-only access to your Search Console data, encrypts your Google credentials, and lets you disconnect and fully remove all search data at any time.
|
||||
- **Integrations tab in Settings.** A new "Integrations" section in your site settings is where you connect and manage external services. Google Search Console is the first integration available — more will follow.
|
||||
- **Search performance on your dashboard.** When Google Search Console is connected, a new "Search" panel appears on your main dashboard alongside Campaigns — showing your total clicks, impressions, and average position at a glance, plus your top 5 search queries. Click "View all" to dive deeper.
|
||||
- **Clicks & Impressions trend chart.** The Search tab now includes a chart showing how your clicks and impressions change over time, so you can spot trends and correlate them with content changes.
|
||||
- **Top query position cards.** Five compact cards at the top of the Search tab show your best-performing queries with their average ranking position and click count.
|
||||
- **New queries indicator.** A green badge on the Search tab tells you how many new search queries appeared this period compared to the last — so you can see your search footprint growing.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ const PerformanceStats = dynamic(() => import('@/components/dashboard/Performanc
|
||||
const GoalStats = dynamic(() => import('@/components/dashboard/GoalStats'))
|
||||
const Campaigns = dynamic(() => import('@/components/dashboard/Campaigns'))
|
||||
const PeakHours = dynamic(() => import('@/components/dashboard/PeakHours'))
|
||||
const SearchPerformance = dynamic(() => import('@/components/dashboard/SearchPerformance'))
|
||||
const EventProperties = dynamic(() => import('@/components/dashboard/EventProperties'))
|
||||
const ExportModal = dynamic(() => import('@/components/dashboard/ExportModal'))
|
||||
import { type DimensionFilter, serializeFilters, parseFiltersFromURL } from '@/lib/filters'
|
||||
@@ -614,6 +615,9 @@ export default function SiteDashboardPage() {
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2 mb-8">
|
||||
<Campaigns siteId={siteId} dateRange={dateRange} filters={filtersParam || undefined} onFilter={handleAddFilter} />
|
||||
<SearchPerformance siteId={siteId} dateRange={dateRange} />
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<PeakHours siteId={siteId} dateRange={dateRange} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import { useParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { getDateRange, formatDate, Select, DatePicker } from '@ciphera-net/ui'
|
||||
import { CaretDown, CaretUp, MagnifyingGlass, ArrowSquareOut } from '@phosphor-icons/react'
|
||||
import { useDashboard, useGSCStatus, useGSCOverview, useGSCTopQueries, useGSCTopPages } from '@/lib/swr/dashboard'
|
||||
import { useDashboard, useGSCStatus, useGSCOverview, useGSCTopQueries, useGSCTopPages, useGSCNewQueries } from '@/lib/swr/dashboard'
|
||||
import { getGSCQueryPages, getGSCPageQueries } from '@/lib/api/gsc'
|
||||
import type { GSCDataRow } from '@/lib/api/gsc'
|
||||
import { SkeletonLine, StatCardSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
||||
import ClicksImpressionsChart from '@/components/search/ClicksImpressionsChart'
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
@@ -73,6 +74,7 @@ export default function SearchConsolePage() {
|
||||
const { data: overview } = useGSCOverview(siteId, dateRange.start, dateRange.end)
|
||||
const { data: topQueries, isLoading: queriesLoading } = useGSCTopQueries(siteId, dateRange.start, dateRange.end, PAGE_SIZE, queryPage * PAGE_SIZE)
|
||||
const { data: topPages, isLoading: pagesLoading } = useGSCTopPages(siteId, dateRange.start, dateRange.end, PAGE_SIZE, pagePage * PAGE_SIZE)
|
||||
const { data: newQueries } = useGSCNewQueries(siteId, dateRange.start, dateRange.end)
|
||||
|
||||
const showSkeleton = useMinimumLoading(!gscStatus)
|
||||
const fadeClass = useSkeletonFade(showSkeleton)
|
||||
@@ -287,6 +289,32 @@ export default function SearchConsolePage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClicksImpressionsChart siteId={siteId} startDate={dateRange.start} endDate={dateRange.end} />
|
||||
|
||||
{/* Position tracker */}
|
||||
{topQueries?.queries && topQueries.queries.length > 0 && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 mb-6">
|
||||
{topQueries.queries.slice(0, 5).map((q) => (
|
||||
<div key={q.query} className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3">
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate mb-1">{q.query}</p>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<p className="text-lg font-semibold text-neutral-900 dark:text-white">{q.position.toFixed(1)}</p>
|
||||
<p className="text-xs text-neutral-400">pos</p>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{q.clicks} {q.clicks === 1 ? 'click' : 'clicks'}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New queries badge */}
|
||||
{newQueries && newQueries.count > 0 && (
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 text-sm mb-4">
|
||||
<span className="font-medium">{newQueries.count} new {newQueries.count === 1 ? 'query' : 'queries'}</span>
|
||||
<span className="text-green-600 dark:text-green-400">appeared this period</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View toggle */}
|
||||
<div className="mb-6">
|
||||
<div className="inline-flex bg-neutral-100 dark:bg-neutral-800 rounded-lg p-1">
|
||||
|
||||
125
components/dashboard/SearchPerformance.tsx
Normal file
125
components/dashboard/SearchPerformance.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { MagnifyingGlass, CaretUp, CaretDown } from '@phosphor-icons/react'
|
||||
import { useGSCStatus, useGSCOverview, useGSCTopQueries } from '@/lib/swr/dashboard'
|
||||
|
||||
interface SearchPerformanceProps {
|
||||
siteId: string
|
||||
dateRange: { start: string; end: string }
|
||||
}
|
||||
|
||||
function ChangeArrow({ current, previous, invert = false }: { current: number; previous: number; invert?: boolean }) {
|
||||
if (!previous || previous === 0) return null
|
||||
const improved = invert ? current < previous : current > previous
|
||||
const same = current === previous
|
||||
if (same) return null
|
||||
return improved ? (
|
||||
<CaretUp className="w-3 h-3 text-emerald-500" weight="fill" />
|
||||
) : (
|
||||
<CaretDown className="w-3 h-3 text-red-500" weight="fill" />
|
||||
)
|
||||
}
|
||||
|
||||
export default function SearchPerformance({ siteId, dateRange }: SearchPerformanceProps) {
|
||||
const { data: gscStatus } = useGSCStatus(siteId)
|
||||
const { data: overview, isLoading: overviewLoading } = useGSCOverview(siteId, dateRange.start, dateRange.end)
|
||||
const { data: queriesData, isLoading: queriesLoading } = useGSCTopQueries(siteId, dateRange.start, dateRange.end, 5, 0)
|
||||
|
||||
// Don't render if GSC is not connected
|
||||
if (!gscStatus?.connected) return null
|
||||
|
||||
const isLoading = overviewLoading || queriesLoading
|
||||
const queries = queriesData?.queries ?? []
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MagnifyingGlass className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Search
|
||||
</h3>
|
||||
</div>
|
||||
<Link
|
||||
href={`/sites/${siteId}/search`}
|
||||
className="text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
|
||||
>
|
||||
View all →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
/* Loading skeleton */
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-4 w-20 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-24 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-20 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="h-9 bg-neutral-100 dark:bg-neutral-800 rounded-lg animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Inline stats row */}
|
||||
<div className="flex items-center gap-5 mb-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Clicks</span>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
{(overview?.total_clicks ?? 0).toLocaleString()}
|
||||
</span>
|
||||
<ChangeArrow current={overview?.total_clicks ?? 0} previous={overview?.prev_clicks ?? 0} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Impressions</span>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
{(overview?.total_impressions ?? 0).toLocaleString()}
|
||||
</span>
|
||||
<ChangeArrow current={overview?.total_impressions ?? 0} previous={overview?.prev_impressions ?? 0} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Avg Position</span>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
{(overview?.avg_position ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<ChangeArrow current={overview?.avg_position ?? 0} previous={overview?.prev_avg_position ?? 0} invert />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 5 queries list */}
|
||||
<div className="space-y-1 flex-1">
|
||||
{queries.length > 0 ? (
|
||||
queries.map((q) => (
|
||||
<div
|
||||
key={q.query}
|
||||
className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors"
|
||||
>
|
||||
<span className="text-sm text-neutral-900 dark:text-white truncate flex-1 min-w-0" title={q.query}>
|
||||
{q.query}
|
||||
</span>
|
||||
<div className="flex items-center gap-3 ml-4 shrink-0">
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
{q.clicks.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-400 dark:text-neutral-500 bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded font-medium">
|
||||
{q.position.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center py-6">
|
||||
<p className="text-sm text-neutral-400 dark:text-neutral-500">No search data yet</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
189
components/search/ClicksImpressionsChart.tsx
Normal file
189
components/search/ClicksImpressionsChart.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useTheme } from '@ciphera-net/ui'
|
||||
import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from 'recharts'
|
||||
import { ChartContainer, ChartTooltip, type ChartConfig } from '@/components/ui/line-charts-6'
|
||||
import { useGSCDailyTotals } from '@/lib/swr/dashboard'
|
||||
import { SkeletonLine } from '@/components/skeletons'
|
||||
import { formatDateShort } from '@/lib/utils/formatDate'
|
||||
|
||||
// ─── Config ─────────────────────────────────────────────────────
|
||||
|
||||
const chartConfig = {
|
||||
clicks: { label: 'Clicks', color: '#FD5E0F' },
|
||||
impressions: { label: 'Impressions', color: '#9CA3AF' },
|
||||
} satisfies ChartConfig
|
||||
|
||||
// ─── Custom Tooltip ─────────────────────────────────────────────
|
||||
|
||||
interface TooltipProps {
|
||||
active?: boolean
|
||||
payload?: Array<{ dataKey: string; value: number; color: string }>
|
||||
label?: string
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }: TooltipProps) {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
const clicks = payload.find((p) => p.dataKey === 'clicks')
|
||||
const impressions = payload.find((p) => p.dataKey === 'impressions')
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 p-3 shadow-sm shadow-black/5 min-w-[140px]">
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400 mb-1.5">{label}</div>
|
||||
{clicks && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#FD5E0F' }} />
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Clicks:</span>
|
||||
<span className="font-semibold text-neutral-900 dark:text-white">{clicks.value.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
{impressions && (
|
||||
<div className="flex items-center gap-2 text-sm mt-1">
|
||||
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#9CA3AF' }} />
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Impressions:</span>
|
||||
<span className="font-semibold text-neutral-900 dark:text-white">{impressions.value.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Component ──────────────────────────────────────────────────
|
||||
|
||||
interface ClicksImpressionsChartProps {
|
||||
siteId: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
}
|
||||
|
||||
export default function ClicksImpressionsChart({ siteId, startDate, endDate }: ClicksImpressionsChartProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { data, isLoading } = useGSCDailyTotals(siteId, startDate, endDate)
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data?.daily_totals?.length) return []
|
||||
return data.daily_totals.map((item) => ({
|
||||
date: formatDateShort(new Date(item.date + 'T00:00:00')),
|
||||
clicks: item.clicks,
|
||||
impressions: item.impressions,
|
||||
}))
|
||||
}, [data])
|
||||
|
||||
// Loading skeleton
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 mb-6">
|
||||
<SkeletonLine className="h-4 w-36 mb-3" />
|
||||
<SkeletonLine className="h-64 w-full rounded-lg" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// No data — don't render anything
|
||||
if (!chartData.length) return null
|
||||
|
||||
const gridStroke = resolvedTheme === 'dark' ? '#374151' : '#e5e7eb'
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 mb-6">
|
||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-3">
|
||||
Clicks & Impressions
|
||||
</p>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-64 w-full [&_.recharts-curve.recharts-tooltip-cursor]:stroke-[initial]"
|
||||
>
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 8, right: 8, left: 0, bottom: 8 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="clicksFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#FD5E0F" stopOpacity={0.15} />
|
||||
<stop offset="100%" stopColor="#FD5E0F" stopOpacity={0.01} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<CartesianGrid
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
stroke={gridStroke}
|
||||
strokeOpacity={0.7}
|
||||
/>
|
||||
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 11, fill: 'var(--chart-axis)' }}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 11, fill: 'var(--chart-axis)' }}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
tickFormatter={(v: number) => v.toLocaleString()}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 11, fill: 'var(--chart-axis)' }}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
tickFormatter={(v: number) => v.toLocaleString()}
|
||||
/>
|
||||
|
||||
<ChartTooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3', stroke: '#9ca3af' }} />
|
||||
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="bump"
|
||||
dataKey="clicks"
|
||||
fill="url(#clicksFill)"
|
||||
stroke="none"
|
||||
/>
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="bump"
|
||||
dataKey="clicks"
|
||||
stroke="#FD5E0F"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{
|
||||
r: 5,
|
||||
fill: '#FD5E0F',
|
||||
stroke: 'white',
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Line
|
||||
yAxisId="right"
|
||||
type="bump"
|
||||
dataKey="impressions"
|
||||
stroke="#9CA3AF"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
strokeDasharray="4 3"
|
||||
activeDot={{
|
||||
r: 5,
|
||||
fill: '#9CA3AF',
|
||||
stroke: 'white',
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -77,3 +77,22 @@ export async function getGSCQueryPages(siteId: string, query: string, startDate:
|
||||
export async function getGSCPageQueries(siteId: string, page: string, startDate: string, endDate: string): Promise<GSCQueryResponse> {
|
||||
return apiRequest<GSCQueryResponse>(`/sites/${siteId}/gsc/page-queries?page=${encodeURIComponent(page)}&start_date=${startDate}&end_date=${endDate}`)
|
||||
}
|
||||
|
||||
export interface GSCDailyTotal {
|
||||
date: string
|
||||
clicks: number
|
||||
impressions: number
|
||||
}
|
||||
|
||||
export interface GSCNewQueries {
|
||||
count: number
|
||||
queries: string[]
|
||||
}
|
||||
|
||||
export async function getGSCDailyTotals(siteId: string, startDate: string, endDate: string): Promise<{ daily_totals: GSCDailyTotal[] }> {
|
||||
return apiRequest<{ daily_totals: GSCDailyTotal[] }>(`/sites/${siteId}/gsc/daily-totals?start_date=${startDate}&end_date=${endDate}`)
|
||||
}
|
||||
|
||||
export async function getGSCNewQueries(siteId: string, startDate: string, endDate: string): Promise<GSCNewQueries> {
|
||||
return apiRequest<GSCNewQueries>(`/sites/${siteId}/gsc/new-queries?start_date=${startDate}&end_date=${endDate}`)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ import { listFunnels, type Funnel } from '@/lib/api/funnels'
|
||||
import { getUptimeStatus, type UptimeStatusResponse } from '@/lib/api/uptime'
|
||||
import { listGoals, type Goal } from '@/lib/api/goals'
|
||||
import { listReportSchedules, type ReportSchedule } from '@/lib/api/report-schedules'
|
||||
import { getGSCStatus, getGSCOverview, getGSCTopQueries, getGSCTopPages } from '@/lib/api/gsc'
|
||||
import type { GSCStatus, GSCOverview, GSCQueryResponse, GSCPageResponse } from '@/lib/api/gsc'
|
||||
import { getGSCStatus, getGSCOverview, getGSCTopQueries, getGSCTopPages, getGSCDailyTotals, getGSCNewQueries } from '@/lib/api/gsc'
|
||||
import type { GSCStatus, GSCOverview, GSCQueryResponse, GSCPageResponse, GSCDailyTotal, GSCNewQueries } from '@/lib/api/gsc'
|
||||
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
|
||||
import type {
|
||||
Stats,
|
||||
@@ -84,6 +84,8 @@ const fetchers = {
|
||||
gscOverview: (siteId: string, start: string, end: string) => getGSCOverview(siteId, start, end),
|
||||
gscTopQueries: (siteId: string, start: string, end: string, limit: number, offset: number) => getGSCTopQueries(siteId, start, end, limit, offset),
|
||||
gscTopPages: (siteId: string, start: string, end: string, limit: number, offset: number) => getGSCTopPages(siteId, start, end, limit, offset),
|
||||
gscDailyTotals: (siteId: string, start: string, end: string) => getGSCDailyTotals(siteId, start, end),
|
||||
gscNewQueries: (siteId: string, start: string, end: string) => getGSCNewQueries(siteId, start, end),
|
||||
subscription: () => getSubscription(),
|
||||
}
|
||||
|
||||
@@ -449,6 +451,24 @@ export function useGSCTopPages(siteId: string, start: string, end: string, limit
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for GSC daily totals (clicks & impressions per day)
|
||||
export function useGSCDailyTotals(siteId: string, start: string, end: string) {
|
||||
return useSWR<{ daily_totals: GSCDailyTotal[] }>(
|
||||
siteId && start && end ? ['gscDailyTotals', siteId, start, end] : null,
|
||||
() => fetchers.gscDailyTotals(siteId, start, end),
|
||||
dashboardSWRConfig
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for GSC new queries (queries that appeared in the current period)
|
||||
export function useGSCNewQueries(siteId: string, start: string, end: string) {
|
||||
return useSWR<GSCNewQueries>(
|
||||
siteId && start && end ? ['gscNewQueries', siteId, start, end] : null,
|
||||
() => fetchers.gscNewQueries(siteId, start, end),
|
||||
dashboardSWRConfig
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for subscription details (changes rarely)
|
||||
export function useSubscription() {
|
||||
return useSWR<SubscriptionDetails>(
|
||||
|
||||
Reference in New Issue
Block a user