'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 } from '@ciphera-net/ui' import { ListSkeleton } from '@/components/skeletons' import { getCampaigns, CampaignStat } from '@/lib/api/stats' import { getReferrerFavicon, getReferrerIcon, getReferrerDisplayName } from '@/lib/utils/icons' import { Megaphone, FrameCornersIcon } from '@phosphor-icons/react' import UtmBuilder from '@/components/tools/UtmBuilder' import { type DimensionFilter } from '@/lib/filters' interface CampaignsProps { siteId: string dateRange: { start: string, end: string } filters?: string onFilter?: (filter: DimensionFilter) => void } const LIMIT = 7 export default function Campaigns({ siteId, dateRange, filters, onFilter }: 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 [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( () => [...data].sort((a, b) => b.visitors - a.visitors), [data] ) const sortedFullData = useMemo( () => [...(fullData.length > 0 ? fullData : data)].sort((a, b) => b.visitors - a.visitors), [fullData, data] ) const totalVisitors = sortedData.reduce((sum, c) => sum + c.visitors, 0) const hasData = data.length > 0 const displayedData = hasData ? sortedData.slice(0, LIMIT) : [] const showViewAll = hasData && data.length > LIMIT const emptySlots = Math.max(0, LIMIT - displayedData.length) 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 = sortedFullData.length > 0 ? sortedFullData : sortedData if (rows.length === 0) return const header = ['Source', 'Medium', 'Campaign', 'Visitors', 'Pageviews'] const csvRows = [ header.join(','), ...rows.map(r => [r.source, r.medium || '', r.campaign || '', 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) } return ( <>

Campaigns

{showViewAll && ( )}
{isLoading ? ( ) : hasData ? ( <> {displayedData.map((item) => { return (
onFilter?.({ dimension: 'utm_source', operator: 'is', values: [item.source] })} className={`flex items-center justify-between py-1.5 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`} >
{renderSourceIcon(item.source)}
{getReferrerDisplayName(item.source)}
{item.medium || '—'} · {item.campaign || '—'}
{totalVisitors > 0 ? `${Math.round((item.visitors / totalVisitors) * 100)}%` : ''} {formatNumber(item.visitors)}
) })} {Array.from({ length: emptySlots }).map((_, i) => (
setIsModalOpen(false)} title="All Campaigns" >
{isLoadingFull ? (
) : ( <>
{sortedFullData.map((item) => { return (
{renderSourceIcon(item.source)}
{getReferrerDisplayName(item.source)}
{item.medium || '—'} · {item.campaign || '—'}
{formatNumber(item.visitors)} {formatNumber(item.pageviews)} pv
) })} )}
setIsBuilderOpen(false)} title="Campaign URL Builder" >
) }