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..580466f --- /dev/null +++ b/components/dashboard/Campaigns.tsx @@ -0,0 +1,172 @@ +'use client' + +import { useState, useEffect } from 'react' +import Link from 'next/link' +import { formatNumber } from '@/lib/utils/format' +import { Modal, ArrowRightIcon } from '@ciphera-net/ui' +import { getCampaigns, CampaignStat } from '@/lib/api/stats' +import { FaBullhorn } from 'react-icons/fa' + +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, 10) + 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, 100) + 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) => ( + + ) : ( +
+
+ +
+

+ Track your marketing campaigns +

+

+ Add utm_source, utm_medium, and utm_campaign parameters to your links to see them here. +

+ + Read documentation + + +
+ )} +
+ + 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..205a166 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,31 @@ 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, 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, 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 || []) +} + export interface DashboardData { site: Site stats: Stats