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,13 +213,16 @@ 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)
return (
<> <>
<div className="flex items-center justify-end mb-2"> <div className="flex items-center justify-end mb-2">
<button <button
@@ -229,11 +232,11 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
Export CSV Export CSV
</button> </button>
</div> </div>
{sortedFullData.map((item) => { {sortedFullData.map((item) => (
return (
<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} <div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center">
<ArrowUpRightIcon className="w-3 h-3 ml-2 text-neutral-400 opacity-0 group-hover:opacity-100 transition-opacity" /> <span className="truncate">{page.path}</span>
</a>
</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">
<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)} {formatNumber(page.pageviews)}
</span>
</div> </div>
</div> </div>
)) )
)} })
})()}
</div> </div>
</Modal> </Modal>
</> </>

View File

@@ -322,15 +322,26 @@ 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)
return modalData.map((item) => {
const dim = TAB_TO_DIMENSION[activeTab]
const filterValue = activeTab === 'countries' ? item.country : activeTab === 'regions' ? item.region : item.city
const canFilter = onFilter && dim && filterValue
return (
<div
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"> <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="shrink-0">{getFlagComponent(item.country ?? '')}</span>
<span className="truncate"> <span className="truncate">
@@ -339,12 +350,18 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
getCityName(item.city ?? '')} getCityName(item.city ?? '')}
</span> </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">
<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)} {formatNumber(item.pageviews)}
</span>
</div> </div>
</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)
const dim = TAB_TO_DIMENSION[activeTab]
return modalData.map((item) => {
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"> <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>} {item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{capitalize(item.name)}</span> <span className="truncate">{capitalize(item.name)}</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">
<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)} {formatNumber(item.pageviews)}
</span>
</div> </div>
</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">
<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)} {formatNumber(ref.pageviews)}
</span>
</div> </div>
</div> </div>
)) ))
)} })()}
</div> </div>
</Modal> </Modal>
</> </>