From d5ea1a793bb1d23cf8497b88fd5341b17e9e6ff1 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 19:11:03 +0100 Subject: [PATCH 1/3] feat: add Campaigns component and API integration for campaign statistics in SiteDashboardPage --- app/sites/[id]/page.tsx | 6 ++ components/dashboard/Campaigns.tsx | 157 +++++++++++++++++++++++++++++ lib/api/stats.ts | 31 ++++++ 3 files changed, 194 insertions(+) create mode 100644 components/dashboard/Campaigns.tsx diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 7f674eb..d3ffc9e 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -18,6 +18,7 @@ import TechSpecs from '@/components/dashboard/TechSpecs' import Chart from '@/components/dashboard/Chart' import PerformanceStats from '@/components/dashboard/PerformanceStats' import GoalStats from '@/components/dashboard/GoalStats' +import Campaigns from '@/components/dashboard/Campaigns' export default function SiteDashboardPage() { const { user } = useAuth() @@ -377,6 +378,11 @@ export default function SiteDashboardPage() { /> + {/* Campaigns Report */} +
+ +
+
diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx new file mode 100644 index 0000000..96e7465 --- /dev/null +++ b/components/dashboard/Campaigns.tsx @@ -0,0 +1,157 @@ +'use client' + +import { useState, useEffect } from 'react' +import { formatNumber } from '@/lib/utils/format' +import { Modal } from '@ciphera-net/ui' +import { getCampaigns, CampaignStat } from '@/lib/api/stats' + +interface CampaignsProps { + siteId: string + dateRange: { start: string, end: string } +} + +const LIMIT = 7 + +export default function Campaigns({ siteId, dateRange }: CampaignsProps) { + const [data, setData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [isModalOpen, setIsModalOpen] = useState(false) + const [fullData, setFullData] = useState([]) + const [isLoadingFull, setIsLoadingFull] = useState(false) + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true) + try { + const result = await getCampaigns(siteId, dateRange.start, dateRange.end) + setData(result) + } catch (e) { + console.error(e) + } finally { + setIsLoading(false) + } + } + fetchData() + }, [siteId, dateRange]) + + useEffect(() => { + if (isModalOpen) { + const fetchFullData = async () => { + setIsLoadingFull(true) + try { + const result = await getCampaigns(siteId, dateRange.start, dateRange.end) + setFullData(result) + } catch (e) { + console.error(e) + } finally { + setIsLoadingFull(false) + } + } + fetchFullData() + } else { + setFullData([]) + } + }, [isModalOpen, siteId, dateRange]) + + const hasData = data.length > 0 + const displayedData = hasData ? data.slice(0, LIMIT) : [] + const emptySlots = Math.max(0, LIMIT - displayedData.length) + const showViewAll = hasData && data.length > LIMIT + + return ( + <> +
+
+

+ Campaigns +

+ {showViewAll && ( + + )} +
+ +
+ {isLoading ? ( +
+

Loading...

+
+ ) : hasData ? ( + <> +
+
Source
+
Medium
+
Campaign
+
Visitors
+
+ {displayedData.map((item, index) => ( +
+
+ {item.source} +
+
+ {item.medium || '-'} +
+
+ {item.campaign || '-'} +
+
+ {formatNumber(item.visitors)} +
+
+ ))} + {Array.from({ length: emptySlots }).map((_, i) => ( + +
+ + setIsModalOpen(false)} + title="All Campaigns" + > +
+ {isLoadingFull ? ( +
Loading...
+ ) : ( + <> +
+
Source
+
Medium
+
Campaign
+
Visitors
+
+ {(fullData.length > 0 ? fullData : data).map((item, index) => ( +
+
+ {item.source} +
+
+ {item.medium || '-'} +
+
+ {item.campaign || '-'} +
+
+ {formatNumber(item.visitors)} +
+
+ ))} + + )} +
+
+ + ) +} diff --git a/lib/api/stats.ts b/lib/api/stats.ts index 68c6bdd..683ef92 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -39,6 +39,14 @@ export interface GoalCountStat { display_name?: string | null } +export interface CampaignStat { + source: string + medium: string + campaign: string + visitors: number + pageviews: number +} + export interface TopReferrer { referrer: string pageviews: number @@ -268,6 +276,29 @@ export async function getPublicPerformanceByPage( return res?.performance_by_page ?? [] } +export async function getGoalStats(siteId: string, startDate?: string, endDate?: string, limit = 20): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ goal_counts: GoalCountStat[] }>(`/sites/${siteId}/goals/stats?${params.toString()}`).then(r => r?.goal_counts || []) +} + +export async function getCampaigns(siteId: string, startDate?: string, endDate?: string): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + return apiRequest<{ campaigns: CampaignStat[] }>(`/sites/${siteId}/campaigns?${params.toString()}`).then(r => r?.campaigns || []) +} + +export async function getPublicCampaigns(siteId: string, startDate?: string, endDate?: string, auth?: AuthParams): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + appendAuthParams(params, auth) + return apiRequest<{ campaigns: CampaignStat[] }>(`/public/sites/${siteId}/campaigns?${params.toString()}`).then(r => r?.campaigns || []) +} + export interface DashboardData { site: Site stats: Stats From 143561c4ab6a037cc97c2182b204751d654305b9 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 19:30:51 +0100 Subject: [PATCH 2/3] feat: enhance Campaigns component with loading state, user guidance, and documentation link for tracking marketing campaigns --- components/dashboard/Campaigns.tsx | 89 +++++++++++++++++------------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index 96e7465..b2d406c 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -1,9 +1,11 @@ 'use client' import { useState, useEffect } from 'react' +import Link from 'next/link' import { formatNumber } from '@/lib/utils/format' -import { Modal } from '@ciphera-net/ui' +import { Modal, ArrowRightIcon } from '@ciphera-net/ui' import { getCampaigns, CampaignStat } from '@/lib/api/stats' +import { FaBullhorn } from 'react-icons/fa' interface CampaignsProps { siteId: string @@ -75,45 +77,58 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { )}
-
- {isLoading ? ( -
-

Loading...

+ {isLoading ? ( +
+

Loading...

+
+ ) : hasData ? ( +
+
+
Source
+
Medium
+
Campaign
+
Visitors
- ) : hasData ? ( - <> -
-
Source
-
Medium
-
Campaign
-
Visitors
-
- {displayedData.map((item, index) => ( -
-
- {item.source} -
-
- {item.medium || '-'} -
-
- {item.campaign || '-'} -
-
- {formatNumber(item.visitors)} -
+ {displayedData.map((item, index) => ( +
+
+ {item.source}
- ))} - {Array.from({ length: emptySlots }).map((_, i) => ( - + )}
Date: Wed, 4 Feb 2026 19:41:48 +0100 Subject: [PATCH 3/3] feat: update getCampaigns API to include limit parameter and adjust Campaigns component to utilize it for fetching data --- components/dashboard/Campaigns.tsx | 4 ++-- lib/api/stats.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index b2d406c..580466f 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -25,7 +25,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { const fetchData = async () => { setIsLoading(true) try { - const result = await getCampaigns(siteId, dateRange.start, dateRange.end) + const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 10) setData(result) } catch (e) { console.error(e) @@ -41,7 +41,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { const fetchFullData = async () => { setIsLoadingFull(true) try { - const result = await getCampaigns(siteId, dateRange.start, dateRange.end) + const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100) setFullData(result) } catch (e) { console.error(e) diff --git a/lib/api/stats.ts b/lib/api/stats.ts index 683ef92..205a166 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -284,17 +284,19 @@ export async function getGoalStats(siteId: string, startDate?: string, endDate?: return apiRequest<{ goal_counts: GoalCountStat[] }>(`/sites/${siteId}/goals/stats?${params.toString()}`).then(r => r?.goal_counts || []) } -export async function getCampaigns(siteId: string, startDate?: string, endDate?: string): Promise { +export async function getCampaigns(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise { const params = new URLSearchParams() if (startDate) params.append('start_date', startDate) if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) return apiRequest<{ campaigns: CampaignStat[] }>(`/sites/${siteId}/campaigns?${params.toString()}`).then(r => r?.campaigns || []) } -export async function getPublicCampaigns(siteId: string, startDate?: string, endDate?: string, auth?: AuthParams): Promise { +export async function getPublicCampaigns(siteId: string, startDate?: string, endDate?: string, limit = 10, auth?: AuthParams): Promise { const params = new URLSearchParams() if (startDate) params.append('start_date', startDate) if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) appendAuthParams(params, auth) return apiRequest<{ campaigns: CampaignStat[] }>(`/public/sites/${siteId}/campaigns?${params.toString()}`).then(r => r?.campaigns || []) }