Improve expanded modals: wider, taller, hover percentage, click-to-filter
- Widen modals from max-w-lg to max-w-2xl - Increase max height from 60vh to 80vh - Add hover percentage on each row (matching card behavior) - Click any row to filter dashboard and close modal
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user