Release 0.14.0-alpha #42

Merged
uz1mani merged 109 commits from staging into main 2026-03-12 12:12:03 +00:00
54 changed files with 6104 additions and 1374 deletions
Showing only changes of commit a99d13309f - Show all commits

View File

@@ -213,27 +213,30 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
title="All Campaigns" title="All Campaigns"
className="max-w-2xl"
> >
<div className="space-y-1 max-h-[60vh] overflow-y-auto pr-2"> <div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {isLoadingFull ? (
<div className="py-4"> <div className="py-4">
<ListSkeleton rows={10} /> <ListSkeleton rows={10} />
</div> </div>
) : ( ) : (() => {
<> const modalTotal = sortedFullData.reduce((sum, item) => sum + item.visitors, 0)
<div className="flex items-center justify-end mb-2"> return (
<button <>
onClick={handleExportCampaigns} <div className="flex items-center justify-end mb-2">
className="text-xs font-medium text-neutral-400 hover:text-brand-orange transition-colors cursor-pointer" <button
> onClick={handleExportCampaigns}
Export CSV className="text-xs font-medium text-neutral-400 hover:text-brand-orange transition-colors cursor-pointer"
</button> >
</div> Export CSV
{sortedFullData.map((item) => { </button>
return ( </div>
{sortedFullData.map((item) => (
<div <div
key={`${item.source}|${item.medium}|${item.campaign}`} key={`${item.source}|${item.medium}|${item.campaign}`}
className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors" onClick={() => { if (onFilter) { onFilter({ dimension: 'utm_source', operator: 'is', values: [item.source] }); setIsModalOpen(false) } }}
className={`flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
> >
<div className="flex-1 flex items-center gap-3 min-w-0"> <div className="flex-1 flex items-center gap-3 min-w-0">
{renderSourceIcon(item.source)} {renderSourceIcon(item.source)}
@@ -249,6 +252,9 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
</div> </div>
</div> </div>
<div className="flex items-center gap-4 ml-4 text-sm"> <div className="flex items-center gap-4 ml-4 text-sm">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((item.visitors / modalTotal) * 100)}%` : ''}
</span>
<span className="font-semibold text-neutral-900 dark:text-white"> <span className="font-semibold text-neutral-900 dark:text-white">
{formatNumber(item.visitors)} {formatNumber(item.visitors)}
</span> </span>
@@ -257,10 +263,10 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
</span> </span>
</div> </div>
</div> </div>
) ))}
})} </>
</> )
)} })()}
</div> </div>
</Modal> </Modal>

View File

@@ -197,32 +197,39 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
title={`Pages - ${getTabLabel(activeTab)}`} title={`Pages - ${getTabLabel(activeTab)}`}
className="max-w-2xl"
> >
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2"> <div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {isLoadingFull ? (
<div className="py-4"> <div className="py-4">
<ListSkeleton rows={10} /> <ListSkeleton rows={10} />
</div> </div>
) : ( ) : (() => {
(fullData.length > 0 ? fullData : data).map((page) => ( const modalData = fullData.length > 0 ? fullData : data
<div key={page.path} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors"> const modalTotal = modalData.reduce((sum, p) => sum + p.pageviews, 0)
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center"> return modalData.map((page) => {
<a const canFilter = onFilter && page.path
href={`https://${domain.replace(/^https?:\/\//, '')}${page.path}`} return (
target="_blank" <div
rel="noopener noreferrer" key={page.path}
className="hover:underline flex items-center" onClick={() => { if (canFilter) { onFilter({ dimension: 'path', operator: 'is', values: [page.path] }); setIsModalOpen(false) } }}
> className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
{page.path} >
<ArrowUpRightIcon className="w-3 h-3 ml-2 text-neutral-400 opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center">
</a> <span className="truncate">{page.path}</span>
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((page.pageviews / modalTotal) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
{formatNumber(page.pageviews)}
</span>
</div>
</div> </div>
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4"> )
{formatNumber(page.pageviews)} })
</div> })()}
</div>
))
)}
</div> </div>
</Modal> </Modal>
</> </>

View File

@@ -322,29 +322,46 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
title={`Locations - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} title={`Locations - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
className="max-w-2xl"
> >
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2"> <div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {isLoadingFull ? (
<div className="py-4"> <div className="py-4">
<ListSkeleton rows={10} /> <ListSkeleton rows={10} />
</div> </div>
) : ( ) : (() => {
(fullData.length > 0 ? fullData : data).map((item) => ( const modalData = fullData.length > 0 ? fullData : data
<div key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors"> const modalTotal = modalData.reduce((sum, item) => sum + item.pageviews, 0)
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3"> return modalData.map((item) => {
<span className="shrink-0">{getFlagComponent(item.country ?? '')}</span> const dim = TAB_TO_DIMENSION[activeTab]
<span className="truncate"> const filterValue = activeTab === 'countries' ? item.country : activeTab === 'regions' ? item.region : item.city
{activeTab === 'countries' ? getCountryName(item.country ?? '') : const canFilter = onFilter && dim && filterValue
activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') : return (
getCityName(item.city ?? '')} <div
</span> key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`}
onClick={() => { if (canFilter) { onFilter({ dimension: dim, operator: 'is', values: [filterValue!] }); setIsModalOpen(false) } }}
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
>
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
<span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>
<span className="truncate">
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') :
getCityName(item.city ?? '')}
</span>
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((item.pageviews / modalTotal) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
{formatNumber(item.pageviews)}
</span>
</div>
</div> </div>
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4"> )
{formatNumber(item.pageviews)} })
</div> })()}
</div>
))
)}
</div> </div>
</Modal> </Modal>
</> </>

View File

@@ -223,25 +223,41 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
title={`Technology - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} title={`Technology - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
className="max-w-2xl"
> >
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2"> <div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {isLoadingFull ? (
<div className="py-4"> <div className="py-4">
<ListSkeleton rows={10} /> <ListSkeleton rows={10} />
</div> </div>
) : ( ) : (() => {
(fullData.length > 0 ? fullData : data).map((item) => ( const modalData = fullData.length > 0 ? fullData : data
<div key={item.name} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors"> const modalTotal = modalData.reduce((sum, item) => sum + item.pageviews, 0)
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3"> const dim = TAB_TO_DIMENSION[activeTab]
{item.icon && <span className="text-lg">{item.icon}</span>} return modalData.map((item) => {
<span className="truncate">{capitalize(item.name)}</span> const canFilter = onFilter && dim
return (
<div
key={item.name}
onClick={() => { if (canFilter) { onFilter({ dimension: dim, operator: 'is', values: [item.name] }); setIsModalOpen(false) } }}
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
>
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{capitalize(item.name)}</span>
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((item.pageviews / modalTotal) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
{formatNumber(item.pageviews)}
</span>
</div>
</div> </div>
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4"> )
{formatNumber(item.pageviews)} })
</div> })()}
</div>
))
)}
</div> </div>
</Modal> </Modal>
</> </>

View File

@@ -153,25 +153,37 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
title="Referrers" title="Referrers"
className="max-w-2xl"
> >
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2"> <div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? ( {isLoadingFull ? (
<div className="py-4"> <div className="py-4">
<ListSkeleton rows={10} /> <ListSkeleton rows={10} />
</div> </div>
) : ( ) : (() => {
mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers).map((ref) => ( const modalData = mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers)
<div key={ref.referrer} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors"> const modalTotal = modalData.reduce((sum, r) => sum + r.pageviews, 0)
return modalData.map((ref) => (
<div
key={ref.referrer}
onClick={() => { if (onFilter) { onFilter({ dimension: 'referrer', operator: 'is', values: [ref.referrer] }); setIsModalOpen(false) } }}
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
>
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3"> <div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{renderReferrerIcon(ref.referrer)} {renderReferrerIcon(ref.referrer)}
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span> <span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
</div> </div>
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4"> <div className="flex items-center gap-2 ml-4">
{formatNumber(ref.pageviews)} <span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((ref.pageviews / modalTotal) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
{formatNumber(ref.pageviews)}
</span>
</div> </div>
</div> </div>
)) ))
)} })()}
</div> </div>
</Modal> </Modal>
</> </>