'use client' import { useState, useEffect, useMemo } from 'react' import { logger } from '@/lib/utils/logger' import Link from 'next/link' import Image from 'next/image' import { formatNumber } from '@ciphera-net/ui' import { Modal, ArrowRightIcon, Button } from '@ciphera-net/ui' import { TableSkeleton } from '@/components/skeletons' import { ChevronDownIcon, DownloadIcon } from '@ciphera-net/ui' import { getCampaigns, CampaignStat } from '@/lib/api/stats' import { getReferrerFavicon, getReferrerIcon, getReferrerDisplayName } from '@/lib/utils/icons' import { FaBullhorn } from 'react-icons/fa' import { PlusIcon } from '@radix-ui/react-icons' import UtmBuilder from '@/components/tools/UtmBuilder' interface CampaignsProps { siteId: string dateRange: { start: string, end: string } filters?: string } const LIMIT = 7 const EMPTY_LABEL = '—' type SortKey = 'source' | 'medium' | 'campaign' | 'visitors' | 'pageviews' type SortDir = 'asc' | 'desc' function sortCampaigns(data: CampaignStat[], key: SortKey, dir: SortDir): CampaignStat[] { return [...data].sort((a, b) => { const av = key === 'visitors' ? a.visitors : key === 'pageviews' ? a.pageviews : (a[key] || '').toLowerCase() const bv = key === 'visitors' ? b.visitors : key === 'pageviews' ? b.pageviews : (b[key] || '').toLowerCase() if (typeof av === 'number' && typeof bv === 'number') { return dir === 'asc' ? av - bv : bv - av } const cmp = String(av).localeCompare(String(bv)) return dir === 'asc' ? cmp : -cmp }) } function campaignRowKey(item: CampaignStat): string { return `${item.source}|${item.medium}|${item.campaign}` } export default function Campaigns({ siteId, dateRange, filters }: CampaignsProps) { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isModalOpen, setIsModalOpen] = useState(false) const [isBuilderOpen, setIsBuilderOpen] = useState(false) const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) const [sortKey, setSortKey] = useState('visitors') const [sortDir, setSortDir] = useState('desc') const [faviconFailed, setFaviconFailed] = useState>(new Set()) useEffect(() => { const fetchData = async () => { setIsLoading(true) try { const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 10, filters) setData(result) } catch (e) { logger.error(e) } finally { setIsLoading(false) } } fetchData() }, [siteId, dateRange, filters]) useEffect(() => { if (isModalOpen) { const fetchFullData = async () => { setIsLoadingFull(true) try { const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100, filters) setFullData(result) } catch (e) { logger.error(e) } finally { setIsLoadingFull(false) } } fetchFullData() } else { setFullData([]) } }, [isModalOpen, siteId, dateRange, filters]) const sortedData = useMemo( () => sortCampaigns(data, sortKey, sortDir), [data, sortKey, sortDir] ) const sortedFullData = useMemo( () => sortCampaigns(fullData.length > 0 ? fullData : data, sortKey, sortDir), [fullData, data, sortKey, sortDir] ) const hasData = data.length > 0 const displayedData = hasData ? sortedData.slice(0, LIMIT) : [] const emptySlots = Math.max(0, LIMIT - displayedData.length) const showViewAll = hasData && data.length > LIMIT const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDir(d => d === 'asc' ? 'desc' : 'asc') } else { setSortKey(key) setSortDir(key === 'visitors' || key === 'pageviews' ? 'desc' : 'asc') } } function renderSourceIcon(source: string) { const faviconUrl = getReferrerFavicon(source) const useFavicon = faviconUrl && !faviconFailed.has(source) if (useFavicon) { return ( setFaviconFailed((prev) => new Set(prev).add(source))} unoptimized /> ) } return {getReferrerIcon(source)} } const handleExportCampaigns = () => { const rows = sortedData.length > 0 ? sortedData : data if (rows.length === 0) return const header = ['Source', 'Medium', 'Campaign', 'Visitors', 'Pageviews'] const csvRows = [ header.join(','), ...rows.map(r => [r.source, r.medium || EMPTY_LABEL, r.campaign || EMPTY_LABEL, r.visitors, r.pageviews].join(',') ), ] const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.setAttribute('href', url) link.setAttribute('download', `campaigns_${dateRange.start}_${dateRange.end}.csv`) document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } const SortHeader = ({ label, colKey, className = '' }: { label: string; colKey: SortKey; className?: string }) => ( ) return ( <>

Campaigns

{hasData && ( )} {showViewAll && ( )}
{isLoading ? (
{Array.from({ length: 7 }).map((_, i) => (
))}
) : hasData ? (
{displayedData.map((item) => (
{renderSourceIcon(item.source)} {getReferrerDisplayName(item.source)}
{item.medium || EMPTY_LABEL}
{item.campaign || EMPTY_LABEL}
{formatNumber(item.visitors)}
{formatNumber(item.pageviews)}
))} {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 ? (
) : ( <>
Source
Medium
Campaign
Visitors
Pageviews
{sortedFullData.map((item) => (
{renderSourceIcon(item.source)} {getReferrerDisplayName(item.source)}
{item.medium || EMPTY_LABEL}
{item.campaign || EMPTY_LABEL}
{formatNumber(item.visitors)}
{formatNumber(item.pageviews)}
))} )}
setIsBuilderOpen(false)} title="Campaign URL Builder" >
) }