Replace 'View all' buttons with expand icon in block headers
Add ArrowsOutSimpleIcon next to each panel title (Pages, Referrers, Locations, Technology, Campaigns) that opens the full-data modal. Remove the old text-based 'View all' button from the bottom of lists. Update changelog with performance and UI improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ 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 } from '@phosphor-icons/react'
|
||||
import { Megaphone, ArrowsOutSimpleIcon } from '@phosphor-icons/react'
|
||||
import UtmBuilder from '@/components/tools/UtmBuilder'
|
||||
import { type DimensionFilter } from '@/lib/filters'
|
||||
|
||||
@@ -124,9 +124,20 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
<>
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Campaigns
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Campaigns
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1 text-neutral-300 dark:text-neutral-600 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded"
|
||||
aria-label="View all campaigns"
|
||||
>
|
||||
<ArrowsOutSimpleIcon className="w-3.5 h-3.5" weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsBuilderOpen(true)}
|
||||
className="text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer"
|
||||
@@ -171,21 +182,9 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{showViewAll ? (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded-lg px-2 -mx-2"
|
||||
>
|
||||
View all
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))
|
||||
)}
|
||||
{Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { logger } from '@/lib/utils/logger'
|
||||
import { formatNumber } from '@ciphera-net/ui'
|
||||
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
|
||||
import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats'
|
||||
import { ArrowsOutSimpleIcon } from '@phosphor-icons/react'
|
||||
import { Modal, ArrowUpRightIcon, LayoutDashboardIcon } from '@ciphera-net/ui'
|
||||
import { ListSkeleton } from '@/components/skeletons'
|
||||
import { type DimensionFilter } from '@/lib/filters'
|
||||
@@ -97,9 +98,20 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
<>
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Pages
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Pages
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1 text-neutral-300 dark:text-neutral-600 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded"
|
||||
aria-label="View all pages"
|
||||
>
|
||||
<ArrowsOutSimpleIcon className="w-3.5 h-3.5" weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1" role="tablist" aria-label="Pages view tabs" onKeyDown={handleTabKeyDown}>
|
||||
{(['top_pages', 'entry_pages', 'exit_pages'] as Tab[]).map((tab) => (
|
||||
<button
|
||||
@@ -161,21 +173,9 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{showViewAll ? (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded-lg px-2 -mx-2"
|
||||
>
|
||||
View all
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))
|
||||
)}
|
||||
{Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
|
||||
@@ -13,7 +13,7 @@ const DottedMap = dynamic(() => import('./DottedMap'), { ssr: false })
|
||||
const Globe = dynamic(() => import('./Globe'), { ssr: false })
|
||||
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
||||
import { ListSkeleton } from '@/components/skeletons'
|
||||
import { ShieldCheck, Detective, Broadcast } from '@phosphor-icons/react'
|
||||
import { ShieldCheck, Detective, Broadcast, ArrowsOutSimpleIcon } from '@phosphor-icons/react'
|
||||
import { getCountries, getCities, getRegions } from '@/lib/api/stats'
|
||||
import { type DimensionFilter } from '@/lib/filters'
|
||||
|
||||
@@ -202,9 +202,20 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
<>
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Locations
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Locations
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1 text-neutral-300 dark:text-neutral-600 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded"
|
||||
aria-label="View all locations"
|
||||
>
|
||||
<ArrowsOutSimpleIcon className="w-3.5 h-3.5" weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1" role="tablist" aria-label="Location view tabs" onKeyDown={handleTabKeyDown}>
|
||||
{(['map', 'globe', 'countries', 'regions', 'cities'] as Tab[]).map((tab) => (
|
||||
<button
|
||||
@@ -286,21 +297,9 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{showViewAll ? (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded-lg px-2 -mx-2"
|
||||
>
|
||||
View all
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))
|
||||
)}
|
||||
{Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { logger } from '@/lib/utils/logger'
|
||||
import { formatNumber } from '@ciphera-net/ui'
|
||||
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
|
||||
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
|
||||
import { Monitor } from '@phosphor-icons/react'
|
||||
import { Monitor, ArrowsOutSimpleIcon } from '@phosphor-icons/react'
|
||||
import { Modal, GridIcon } from '@ciphera-net/ui'
|
||||
import { ListSkeleton } from '@/components/skeletons'
|
||||
import { getBrowsers, getOS, getDevices, getScreenResolutions } from '@/lib/api/stats'
|
||||
@@ -128,9 +128,20 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
<>
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Technology
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Technology
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1 text-neutral-300 dark:text-neutral-600 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded"
|
||||
aria-label="View all technology"
|
||||
>
|
||||
<ArrowsOutSimpleIcon className="w-3.5 h-3.5" weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1" role="tablist" aria-label="Technology view tabs" onKeyDown={handleTabKeyDown}>
|
||||
{(['browsers', 'os', 'devices', 'screens'] as Tab[]).map((tab) => (
|
||||
<button
|
||||
@@ -188,21 +199,9 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{showViewAll ? (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded-lg px-2 -mx-2"
|
||||
>
|
||||
View all
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))
|
||||
)}
|
||||
{Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logger } from '@/lib/utils/logger'
|
||||
import Image from 'next/image'
|
||||
import { formatNumber } from '@ciphera-net/ui'
|
||||
import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons'
|
||||
import { ArrowsOutSimpleIcon } from '@phosphor-icons/react'
|
||||
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
||||
import { ListSkeleton } from '@/components/skeletons'
|
||||
import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
|
||||
@@ -85,9 +86,20 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
||||
<>
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Referrers
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
Referrers
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1 text-neutral-300 dark:text-neutral-600 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded"
|
||||
aria-label="View all referrers"
|
||||
>
|
||||
<ArrowsOutSimpleIcon className="w-3.5 h-3.5" weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 flex-1 min-h-[270px]">
|
||||
@@ -117,21 +129,9 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{showViewAll ? (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1.5 h-9 w-full text-xs font-medium text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange transition-colors cursor-pointer rounded-lg px-2 -mx-2"
|
||||
>
|
||||
View all
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))
|
||||
)}
|
||||
{Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user