feat: glass treatment + dark-only cleanup for dashboard components and navigation
This commit is contained in:
@@ -111,7 +111,7 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg transition-all cursor-pointer ${
|
||||
isOpen
|
||||
? 'bg-brand-orange/10 text-brand-orange border border-brand-orange/30'
|
||||
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700 hover:text-neutral-900 dark:hover:text-white border border-transparent'
|
||||
: 'bg-neutral-800 text-neutral-400 hover:bg-neutral-700 hover:text-white border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
@@ -121,7 +121,7 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 mt-1.5 z-50 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden min-w-[280px]">
|
||||
<div className="absolute top-full left-0 mt-1.5 z-50 bg-neutral-900 border border-neutral-700 rounded-xl shadow-xl overflow-hidden min-w-[280px]">
|
||||
{!selectedDim ? (
|
||||
/* Step 1: Dimension list */
|
||||
<div className="py-1">
|
||||
@@ -129,9 +129,9 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
<button
|
||||
key={dim}
|
||||
onClick={() => setSelectedDim(dim)}
|
||||
className="w-full flex items-center justify-between px-4 py-2.5 text-sm text-left hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors cursor-pointer"
|
||||
className="w-full flex items-center justify-between px-4 py-2.5 text-sm text-left hover:bg-neutral-800 transition-colors cursor-pointer"
|
||||
>
|
||||
<span className="text-neutral-900 dark:text-white font-medium">{DIMENSION_LABELS[dim]}</span>
|
||||
<span className="text-white font-medium">{DIMENSION_LABELS[dim]}</span>
|
||||
<svg className="w-3.5 h-3.5 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
@@ -145,13 +145,13 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
<div className="flex items-center gap-2 px-3 pt-3 pb-2">
|
||||
<button
|
||||
onClick={() => { setSelectedDim(null); setSearch(''); setOperator('is'); setFetchedSuggestions([]) }}
|
||||
className="p-1 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors cursor-pointer rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||
className="p-1 text-neutral-400 hover:text-neutral-300 transition-colors cursor-pointer rounded-md hover:bg-neutral-800"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{DIMENSION_LABELS[selectedDim]}
|
||||
</span>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors cursor-pointer ${
|
||||
operator === op
|
||||
? 'bg-brand-orange text-white'
|
||||
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700'
|
||||
: 'bg-neutral-800 text-neutral-400 hover:bg-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{OPERATOR_LABELS[op]}
|
||||
@@ -189,24 +189,24 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
}
|
||||
}}
|
||||
placeholder={`Search ${DIMENSION_LABELS[selectedDim]?.toLowerCase()}...`}
|
||||
className="w-full px-3 py-2 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/40 focus:border-brand-orange transition-colors"
|
||||
className="w-full px-3 py-2 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/40 focus:border-brand-orange transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Values list */}
|
||||
{isFetching ? (
|
||||
<div className="px-4 py-6 text-center">
|
||||
<div className="inline-block w-4 h-4 border-2 border-neutral-300 dark:border-neutral-600 border-t-brand-orange rounded-full animate-spin" />
|
||||
<div className="inline-block w-4 h-4 border-2 border-neutral-600 border-t-brand-orange rounded-full animate-spin" />
|
||||
</div>
|
||||
) : filtered.length > 0 ? (
|
||||
<div className="max-h-52 overflow-y-auto border-t border-neutral-100 dark:border-neutral-800">
|
||||
<div className="max-h-52 overflow-y-auto border-t border-neutral-800">
|
||||
{filtered.map(s => (
|
||||
<button
|
||||
key={s.value}
|
||||
onClick={() => handleSelectValue(s.value)}
|
||||
className="w-full flex items-center justify-between px-4 py-2 text-sm text-left hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors cursor-pointer"
|
||||
className="w-full flex items-center justify-between px-4 py-2 text-sm text-left hover:bg-neutral-800 transition-colors cursor-pointer"
|
||||
>
|
||||
<span className="truncate text-neutral-900 dark:text-white">{s.label}</span>
|
||||
<span className="truncate text-white">{s.label}</span>
|
||||
{s.count !== undefined && (
|
||||
<span className="text-xs text-neutral-400 dark:text-neutral-500 ml-2 tabular-nums flex-shrink-0">
|
||||
{s.count.toLocaleString()}
|
||||
@@ -216,7 +216,7 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
|
||||
))}
|
||||
</div>
|
||||
) : search.trim() ? (
|
||||
<div className="px-3 py-3 border-t border-neutral-100 dark:border-neutral-800">
|
||||
<div className="px-3 py-3 border-t border-neutral-800">
|
||||
<button
|
||||
onClick={handleSubmitCustom}
|
||||
className="w-full px-3 py-2 text-sm font-medium bg-brand-orange text-white rounded-lg hover:bg-brand-orange/90 transition-colors cursor-pointer"
|
||||
|
||||
@@ -124,17 +124,17 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
|
||||
return (
|
||||
<>
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Megaphone className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Campaigns
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
aria-label="View all campaigns"
|
||||
>
|
||||
<FrameCornersIcon className="w-4 h-4" weight="bold" />
|
||||
@@ -161,13 +161,13 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
<div
|
||||
key={`${item.source}|${item.medium}|${item.campaign}`}
|
||||
onClick={() => onFilter?.({ dimension: 'utm_source', operator: 'is', values: [item.source] })}
|
||||
className={`relative flex items-center justify-between py-1.5 group hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`relative flex items-center justify-between py-1.5 group hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
<div className="relative flex-1 text-neutral-900 dark:text-white flex items-center gap-3 min-w-0">
|
||||
<div className="relative flex-1 text-white flex items-center gap-3 min-w-0">
|
||||
{renderSourceIcon(item.source)}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-sm" title={item.source}>
|
||||
@@ -184,7 +184,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
<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">
|
||||
{totalVisitors > 0 ? `${Math.round((item.visitors / totalVisitors) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(item.visitors)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -197,13 +197,13 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<Megaphone className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<Megaphone className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
Track your marketing campaigns
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
<p className="text-sm text-neutral-400 max-w-xs">
|
||||
Add UTM parameters to your links to see campaign performance here.
|
||||
</p>
|
||||
<button
|
||||
@@ -230,7 +230,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
value={modalSearch}
|
||||
onChange={(e) => setModalSearch(e.target.value)}
|
||||
placeholder="Search campaigns..."
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[80vh]">
|
||||
@@ -262,12 +262,12 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
<div
|
||||
key={`${item.source}|${item.medium}|${item.campaign}`}
|
||||
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 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`flex items-center justify-between py-2 group hover:bg-neutral-800 rounded-lg px-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div className="flex-1 flex items-center gap-3 min-w-0">
|
||||
{renderSourceIcon(item.source)}
|
||||
<div className="min-w-0">
|
||||
<div className="text-neutral-900 dark:text-white font-medium truncate text-sm" title={item.source}>
|
||||
<div className="text-white font-medium truncate text-sm" title={item.source}>
|
||||
{getReferrerDisplayName(item.source)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-neutral-400 dark:text-neutral-500">
|
||||
@@ -281,7 +281,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
|
||||
<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-white">
|
||||
{formatNumber(item.visitors)}
|
||||
</span>
|
||||
<span className="text-neutral-400 dark:text-neutral-500 w-16 text-right">
|
||||
|
||||
@@ -8,10 +8,10 @@ export default function ContentHeader({
|
||||
onMobileMenuOpen: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="shrink-0 flex items-center border-b border-neutral-200/60 dark:border-neutral-800/60 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl px-4 py-3.5 md:hidden">
|
||||
<div className="shrink-0 flex items-center border-b border-neutral-800/60 bg-neutral-900/90 backdrop-blur-xl px-4 py-3.5 md:hidden">
|
||||
<button
|
||||
onClick={onMobileMenuOpen}
|
||||
className="p-2 -ml-2 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white"
|
||||
className="p-2 -ml-2 text-neutral-400 hover:text-white"
|
||||
aria-label="Open navigation"
|
||||
>
|
||||
<MenuIcon className="w-5 h-5" />
|
||||
|
||||
@@ -99,17 +99,17 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
|
||||
return (
|
||||
<>
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Files className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Pages
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
aria-label="View all pages"
|
||||
>
|
||||
<FrameCornersIcon className="w-4 h-4" weight="bold" />
|
||||
@@ -125,8 +125,8 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
aria-selected={activeTab === tab}
|
||||
className={`relative px-2.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
||||
activeTab === tab
|
||||
? 'text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
? 'text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{getTabLabel(tab)}
|
||||
@@ -145,7 +145,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
<div className="space-y-2 flex-1 min-h-[270px]">
|
||||
{!collectPagePaths ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-4">
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm">Page path tracking is disabled in site settings</p>
|
||||
<p className="text-neutral-400 text-sm">Page path tracking is disabled in site settings</p>
|
||||
</div>
|
||||
) : hasData ? (
|
||||
<>
|
||||
@@ -156,13 +156,13 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
<div
|
||||
key={page.path}
|
||||
onClick={() => onFilter?.({ dimension: 'page', operator: 'is', values: [page.path] })}
|
||||
className={`relative flex items-center justify-between h-9 group hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`relative flex items-center justify-between h-9 group hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
<div className="relative flex-1 truncate text-neutral-900 dark:text-white flex items-center">
|
||||
<div className="relative flex-1 truncate text-white flex items-center">
|
||||
<span className="truncate">{page.path}</span>
|
||||
<a
|
||||
href={`https://${domain.replace(/^https?:\/\//, '')}${page.path}`}
|
||||
@@ -178,7 +178,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
<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">
|
||||
{totalPageviews > 0 ? `${Math.round((page.pageviews / totalPageviews) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(page.pageviews)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -191,13 +191,13 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<LayoutDashboardIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<LayoutDashboardIcon className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No page data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
<p className="text-sm text-neutral-400 max-w-xs">
|
||||
Your most visited pages will appear here as traffic arrives.
|
||||
</p>
|
||||
<Link
|
||||
@@ -224,7 +224,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
value={modalSearch}
|
||||
onChange={(e) => setModalSearch(e.target.value)}
|
||||
placeholder="Search pages..."
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[80vh]">
|
||||
@@ -246,16 +246,16 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
|
||||
<div
|
||||
key={page.path}
|
||||
onClick={() => { if (canFilter) { onFilter({ dimension: 'page', 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 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`flex items-center justify-between h-9 group hover:bg-neutral-800 rounded-lg px-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center">
|
||||
<div className="flex-1 truncate text-white flex items-center">
|
||||
<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">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(page.pageviews)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ const Sidebar = dynamic(() => import('./Sidebar'), {
|
||||
// so page content never occupies the sidebar zone
|
||||
loading: () => (
|
||||
<div
|
||||
className="hidden md:block shrink-0 border-r border-neutral-200/60 dark:border-neutral-800/60 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl"
|
||||
className="hidden md:block shrink-0 border-r border-neutral-800/60 bg-neutral-900/90 backdrop-blur-xl"
|
||||
style={{ width: 64 }}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -150,7 +150,7 @@ export default function DottedMap({ data, className, formatValue = formatNumber
|
||||
|
||||
{tooltip && (
|
||||
<div
|
||||
className="fixed z-50 px-2.5 py-1.5 text-xs font-medium text-white bg-neutral-900 dark:bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg pointer-events-none -translate-x-1/2 -translate-y-full -mt-2"
|
||||
className="fixed z-50 px-2.5 py-1.5 text-xs font-medium text-white bg-neutral-800 border border-neutral-700 rounded-lg shadow-lg pointer-events-none -translate-x-1/2 -translate-y-full -mt-2"
|
||||
style={{ left: tooltip.x, top: tooltip.y }}
|
||||
>
|
||||
<span>{getCountryName(tooltip.country)}</span>
|
||||
|
||||
@@ -36,14 +36,14 @@ export default function EventProperties({ siteId, eventName, dateRange, onClose
|
||||
const maxCount = values.length > 0 ? values[0].count : 1
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||
<div className="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Properties: <span className="text-brand-orange">{eventName.replace(/_/g, ' ')}</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors cursor-pointer"
|
||||
className="text-neutral-400 hover:text-neutral-300 transition-colors cursor-pointer"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -54,11 +54,11 @@ export default function EventProperties({ siteId, eventName, dateRange, onClose
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-3">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="h-8 bg-neutral-100 dark:bg-neutral-800 rounded-lg" />
|
||||
<div key={i} className="h-8 bg-neutral-800 rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : keys.length === 0 ? (
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 py-4 text-center">
|
||||
<p className="text-sm text-neutral-400 py-4 text-center">
|
||||
No properties recorded for this event yet.
|
||||
</p>
|
||||
) : (
|
||||
@@ -71,7 +71,7 @@ export default function EventProperties({ siteId, eventName, dateRange, onClose
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full transition-colors cursor-pointer ${
|
||||
selectedKey === k.key
|
||||
? 'bg-brand-orange text-white'
|
||||
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700'
|
||||
: 'bg-neutral-800 text-neutral-400 hover:bg-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{k.key}
|
||||
@@ -84,14 +84,14 @@ export default function EventProperties({ siteId, eventName, dateRange, onClose
|
||||
<div key={v.value} className="flex items-center gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-neutral-900 dark:text-white truncate">
|
||||
<span className="text-sm font-medium text-white truncate">
|
||||
{v.value}
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-brand-orange tabular-nums ml-2">
|
||||
{formatNumber(v.count)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden">
|
||||
<div className="w-full h-1.5 bg-neutral-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-brand-orange/60 rounded-full transition-all"
|
||||
style={{ width: `${(v.count / maxCount) * 100}%` }}
|
||||
|
||||
@@ -20,11 +20,11 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
|
||||
const emptySlots = Math.max(0, 6 - list.length)
|
||||
|
||||
return (
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Goals & Events
|
||||
</h3>
|
||||
</div>
|
||||
@@ -36,10 +36,10 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
|
||||
<div
|
||||
key={row.event_name}
|
||||
onClick={() => onSelectEvent?.(row.event_name)}
|
||||
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${onSelectEvent ? ' cursor-pointer' : ''}`}
|
||||
className={`flex items-center justify-between h-9 group hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${onSelectEvent ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-neutral-900 dark:text-white truncate">
|
||||
<span className="text-sm font-medium text-white truncate">
|
||||
{row.display_name ?? row.event_name.replace(/_/g, ' ')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
|
||||
<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">
|
||||
{total > 0 ? `${Math.round((row.count / total) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 tabular-nums">
|
||||
<span className="text-sm font-semibold text-neutral-400 tabular-nums">
|
||||
{formatNumber(row.count)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -59,14 +59,14 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<BookOpenIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<BookOpenIcon className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
Need help tracking goals?
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
|
||||
Add <code className="px-1.5 py-0.5 rounded bg-neutral-200 dark:bg-neutral-700 text-xs font-mono">pulse.track('event_name')</code> where actions happen on your site, then see counts here. Check our guide for step-by-step instructions.
|
||||
<p className="text-sm text-neutral-400 max-w-md">
|
||||
Add <code className="px-1.5 py-0.5 rounded bg-neutral-700 text-xs font-mono">pulse.track('event_name')</code> where actions happen on your site, then see counts here. Check our guide for step-by-step instructions.
|
||||
</p>
|
||||
<Link
|
||||
href="/installation"
|
||||
|
||||
@@ -90,13 +90,13 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
case 'T1':
|
||||
return <ShieldCheck className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
case 'A1':
|
||||
return <Detective className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
|
||||
return <Detective className="w-5 h-5 text-neutral-400" />
|
||||
case 'A2':
|
||||
return <Broadcast className="w-5 h-5 text-blue-500 dark:text-blue-400" />
|
||||
case 'O1':
|
||||
case 'EU':
|
||||
case 'AP':
|
||||
return <GlobeIcon className="w-5 h-5 text-neutral-500 dark:text-neutral-400" />
|
||||
return <GlobeIcon className="w-5 h-5 text-neutral-400" />
|
||||
}
|
||||
|
||||
const FlagComponent = (Flags as Record<string, React.ComponentType<{ className?: string }>>)[countryCode]
|
||||
@@ -216,17 +216,17 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} 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 ref={containerRef} className="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Locations
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
aria-label="View all locations"
|
||||
>
|
||||
<FrameCornersIcon className="w-4 h-4" weight="bold" />
|
||||
@@ -242,8 +242,8 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
aria-selected={activeTab === tab}
|
||||
className={`relative px-2.5 py-1 text-xs font-medium transition-colors capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
||||
activeTab === tab
|
||||
? 'text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
? 'text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
@@ -262,20 +262,20 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
<div className="space-y-2 flex-1 min-h-[270px]">
|
||||
{isTabDisabled() ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-4">
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm">{getDisabledMessage()}</p>
|
||||
<p className="text-neutral-400 text-sm">{getDisabledMessage()}</p>
|
||||
</div>
|
||||
) : isVisualTab ? (
|
||||
hasData ? (
|
||||
inView ? <DottedMap data={filterUnknown(countries) as { country: string; pageviews: number }[]} /> : null
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No location data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
<p className="text-sm text-neutral-400 max-w-xs">
|
||||
Visitor locations will appear here based on anonymous geographic data.
|
||||
</p>
|
||||
<Link
|
||||
@@ -300,13 +300,13 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
<div
|
||||
key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`}
|
||||
onClick={() => canFilter && onFilter({ dimension: dim, operator: 'is', values: [filterValue!] })}
|
||||
className={`relative flex items-center justify-between h-9 group hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`relative flex items-center justify-between h-9 group hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
<div className="relative flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||
<div className="relative flex-1 truncate text-white flex items-center gap-3">
|
||||
<span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>
|
||||
<span className="truncate">
|
||||
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
|
||||
@@ -318,7 +318,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
<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">
|
||||
{totalPageviews > 0 ? `${Math.round((item.pageviews / totalPageviews) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(item.pageviews)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -331,13 +331,13 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No location data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
<p className="text-sm text-neutral-400 max-w-xs">
|
||||
Visitor locations will appear here based on anonymous geographic data.
|
||||
</p>
|
||||
</div>
|
||||
@@ -358,7 +358,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
value={modalSearch}
|
||||
onChange={(e) => setModalSearch(e.target.value)}
|
||||
placeholder="Search locations..."
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[80vh]">
|
||||
@@ -387,9 +387,9 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
<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 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`flex items-center justify-between h-9 group hover:bg-neutral-800 rounded-lg px-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-white flex items-center gap-3">
|
||||
<span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>
|
||||
<span className="truncate">
|
||||
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
|
||||
@@ -401,7 +401,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
<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">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(item.pageviews)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -131,14 +131,14 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Peak Hours</h3>
|
||||
<h3 className="text-lg font-semibold text-white">Peak Hours</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-5">
|
||||
<p className="text-sm text-neutral-400 mb-5">
|
||||
When your visitors are most active
|
||||
</p>
|
||||
|
||||
@@ -146,8 +146,8 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
<div className="flex-1 min-h-[270px] flex flex-col justify-center gap-1.5">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<div className="w-7 h-3 rounded bg-neutral-200 dark:bg-neutral-800 animate-pulse" />
|
||||
<div className="flex-1 h-5 rounded bg-neutral-200 dark:bg-neutral-800 animate-pulse" />
|
||||
<div className="w-7 h-3 rounded bg-neutral-800 animate-pulse" />
|
||||
<div className="flex-1 h-5 rounded bg-neutral-800 animate-pulse" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
key={`${animKey}-${dayIdx}-${bucket}`}
|
||||
className={[
|
||||
'aspect-square w-full rounded-[4px] border cursor-default transition-transform duration-100',
|
||||
'border-neutral-200 dark:border-neutral-800',
|
||||
'border-neutral-800',
|
||||
isActive ? 'animate-cell-highlight' : '',
|
||||
isHoveredCell ? 'scale-110 z-10 relative' : '',
|
||||
isBestCell && !isHoveredCell ? 'ring-1 ring-brand-orange/40' : '',
|
||||
@@ -201,14 +201,14 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
{Object.entries(BUCKET_LABELS).map(([b, label]) => (
|
||||
<span
|
||||
key={b}
|
||||
className="absolute text-[10px] text-neutral-400 dark:text-neutral-600 -translate-x-1/2"
|
||||
className="absolute text-[10px] text-neutral-600 -translate-x-1/2"
|
||||
style={{ left: `${(Number(b) / BUCKETS) * 100}%` }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
<span
|
||||
className="absolute text-[10px] text-neutral-400 dark:text-neutral-600 -translate-x-full"
|
||||
className="absolute text-[10px] text-neutral-600 -translate-x-full"
|
||||
style={{ left: '100%' }}
|
||||
>
|
||||
24:00
|
||||
@@ -222,7 +222,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
{HIGHLIGHT_COLORS.map((color, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-[10px] h-[10px] rounded-[2px] border border-neutral-200 dark:border-neutral-800"
|
||||
className="w-[10px] h-[10px] rounded-[2px] border border-neutral-800"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
@@ -245,7 +245,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
transform: 'translate(-50%, -100%)',
|
||||
}}
|
||||
>
|
||||
<div className="bg-neutral-900 dark:bg-neutral-800 border border-neutral-700 text-white text-xs px-3 py-2 rounded-lg shadow-xl whitespace-nowrap">
|
||||
<div className="bg-neutral-800 border border-neutral-700 text-white text-xs px-3 py-2 rounded-lg shadow-xl whitespace-nowrap">
|
||||
<div className="font-semibold mb-1">
|
||||
{DAYS[hovered.day]} {formatBucket(hovered.bucket)}
|
||||
</div>
|
||||
@@ -269,7 +269,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.6 }}
|
||||
className="mt-4 text-xs text-neutral-500 dark:text-neutral-400 text-center"
|
||||
className="mt-4 text-xs text-neutral-400 text-center"
|
||||
>
|
||||
Your busiest time is{' '}
|
||||
<span className="text-brand-orange font-medium">
|
||||
@@ -280,7 +280,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center gap-3">
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<p className="text-sm text-neutral-400">
|
||||
No data available for this period
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -9,15 +9,15 @@ interface RealtimeVisitorsProps {
|
||||
export default function RealtimeVisitors({ count }: RealtimeVisitorsProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"
|
||||
className="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<div className="text-sm text-neutral-400">
|
||||
Real-time Visitors
|
||||
</div>
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-neutral-900 dark:text-white">
|
||||
<div className="text-3xl font-bold text-white">
|
||||
<AnimatedNumber value={count} format={(v) => v.toLocaleString()} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,13 @@ export default function ScrollDepth({ goalCounts, totalPageviews }: ScrollDepthP
|
||||
}))
|
||||
|
||||
return (
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Scroll Depth
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
% of visitors who scrolled this far
|
||||
</p>
|
||||
|
||||
@@ -73,13 +73,13 @@ export default function ScrollDepth({ goalCounts, totalPageviews }: ScrollDepthP
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<BarChartIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<BarChartIcon className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No scroll data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
|
||||
<p className="text-sm text-neutral-400 max-w-md">
|
||||
Scroll depth tracking is automatic — data will appear here once visitors start scrolling on your pages.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ function ChangeArrow({ current, previous, invert = false }: { current: number; p
|
||||
function getPositionBadgeClasses(position: number): string {
|
||||
if (position <= 10) return 'text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 dark:bg-emerald-500/20'
|
||||
if (position <= 20) return 'text-brand-orange dark:text-brand-orange bg-brand-orange/10 dark:bg-brand-orange/20'
|
||||
if (position <= 50) return 'text-neutral-400 dark:text-neutral-500 bg-neutral-100 dark:bg-neutral-800'
|
||||
if (position <= 50) return 'text-neutral-400 dark:text-neutral-500 bg-neutral-800'
|
||||
return 'text-red-500 dark:text-red-400 bg-red-500/10 dark:bg-red-500/20'
|
||||
}
|
||||
|
||||
@@ -100,16 +100,16 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
|
||||
return (
|
||||
<>
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MagnifyingGlass className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Search</h3>
|
||||
<h3 className="text-lg font-semibold text-white">Search</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
aria-label="View all search data"
|
||||
>
|
||||
<FrameCornersIcon className="w-4 h-4" weight="bold" />
|
||||
@@ -125,8 +125,8 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
aria-selected={activeTab === tab}
|
||||
className={`relative px-2.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
||||
activeTab === tab
|
||||
? 'text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
? 'text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{getTabLabel(tab)}
|
||||
@@ -145,9 +145,9 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
{isLoading ? (
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-4 w-20 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-24 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-20 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-20 bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-24 bg-neutral-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-20 bg-neutral-800 rounded animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4">
|
||||
<ListSkeleton rows={LIMIT} />
|
||||
@@ -158,22 +158,22 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
{/* Inline stats row */}
|
||||
<div className="flex items-center gap-5 mb-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Clicks</span>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
<span className="text-xs text-neutral-400">Clicks</span>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{formatNumber(overview?.total_clicks ?? 0)}
|
||||
</span>
|
||||
<ChangeArrow current={overview?.total_clicks ?? 0} previous={overview?.prev_clicks ?? 0} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Impressions</span>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
<span className="text-xs text-neutral-400">Impressions</span>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{formatNumber(overview?.total_impressions ?? 0)}
|
||||
</span>
|
||||
<ChangeArrow current={overview?.total_impressions ?? 0} previous={overview?.prev_impressions ?? 0} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Avg Position</span>
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
<span className="text-xs text-neutral-400">Avg Position</span>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{(overview?.avg_position ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<ChangeArrow current={overview?.avg_position ?? 0} previous={overview?.prev_avg_position ?? 0} invert />
|
||||
@@ -191,20 +191,20 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
className="relative flex items-center justify-between h-9 group hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors"
|
||||
className="relative flex items-center justify-between h-9 group hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
<span className="relative text-sm text-neutral-900 dark:text-white truncate flex-1 min-w-0" title={label}>
|
||||
<span className="relative text-sm text-white truncate flex-1 min-w-0" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="relative flex items-center gap-3 ml-4 shrink-0">
|
||||
<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">
|
||||
{totalImpressions > 0 ? `${Math.round((row.impressions / totalImpressions) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(row.clicks)}
|
||||
</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getPositionBadgeClasses(row.position)}`}>
|
||||
@@ -241,7 +241,7 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
value={modalSearch}
|
||||
onChange={(e) => setModalSearch(e.target.value)}
|
||||
placeholder={`Search ${activeTab}...`}
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[80vh]">
|
||||
@@ -266,16 +266,16 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 transition-colors"
|
||||
className="flex items-center justify-between h-9 group hover:bg-neutral-800 rounded-lg px-2 transition-colors"
|
||||
>
|
||||
<span className="flex-1 truncate text-sm text-neutral-900 dark:text-white" title={label}>
|
||||
<span className="flex-1 truncate text-sm text-white" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex items-center gap-3 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((row.impressions / modalTotal) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(row.clicks)}
|
||||
</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getPositionBadgeClasses(row.position)}`}>
|
||||
|
||||
@@ -162,12 +162,12 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
|
||||
setOpen(!open)
|
||||
}
|
||||
}}
|
||||
className="w-full flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 overflow-hidden"
|
||||
className="w-full flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-200 hover:bg-neutral-800 overflow-hidden"
|
||||
>
|
||||
<span className="w-7 h-7 rounded-md bg-brand-orange/10 flex items-center justify-center shrink-0 overflow-hidden">
|
||||
{faviconUrl && !faviconFailed ? (
|
||||
<>
|
||||
{!faviconLoaded && <span className="w-5 h-5 rounded animate-pulse bg-neutral-100 dark:bg-neutral-800" />}
|
||||
{!faviconLoaded && <span className="w-5 h-5 rounded animate-pulse bg-neutral-800" />}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
@@ -187,14 +187,14 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute left-3 top-full mt-1 z-50 w-[240px] bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden">
|
||||
<div className="absolute left-3 top-full mt-1 z-50 w-[240px] bg-neutral-900 border border-neutral-700 rounded-xl shadow-xl overflow-hidden">
|
||||
<div className="p-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search sites..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full px-3 py-1.5 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-neutral-900 dark:text-white placeholder:text-neutral-400"
|
||||
className="w-full px-3 py-1.5 text-sm bg-neutral-800 border border-neutral-700 rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-white placeholder:text-neutral-400"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@@ -206,7 +206,7 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
|
||||
className={`w-full flex items-center gap-2.5 px-4 py-2 text-sm text-left ${
|
||||
site.id === siteId
|
||||
? 'bg-brand-orange/10 text-brand-orange font-medium'
|
||||
: 'text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800'
|
||||
: 'text-neutral-300 hover:bg-neutral-800'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
@@ -222,8 +222,8 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
|
||||
))}
|
||||
{filtered.length === 0 && <p className="px-4 py-3 text-sm text-neutral-400">No sites found</p>}
|
||||
</div>
|
||||
<div className="border-t border-neutral-200 dark:border-neutral-700 p-2">
|
||||
<Link href="/sites/new" onClick={() => setOpen(false)} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg">
|
||||
<div className="border-t border-neutral-700 p-2">
|
||||
<Link href="/sites/new" onClick={() => setOpen(false)} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-neutral-800 rounded-lg">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
Add new site
|
||||
</Link>
|
||||
@@ -256,7 +256,7 @@ function NavLink({
|
||||
className={`flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium overflow-hidden ${
|
||||
active
|
||||
? 'bg-brand-orange/10 text-brand-orange'
|
||||
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800'
|
||||
: 'text-neutral-400 hover:text-white hover:bg-neutral-800'
|
||||
}`}
|
||||
>
|
||||
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
||||
@@ -357,7 +357,7 @@ export default function Sidebar({
|
||||
<span className="w-9 h-9 flex items-center justify-center shrink-0">
|
||||
<img src="/pulse_icon_no_margins.png" alt="Pulse" className="w-9 h-9 shrink-0 object-contain group-hover:scale-105 transition-transform duration-200" />
|
||||
</span>
|
||||
<span className={`text-xl font-bold text-neutral-900 dark:text-white tracking-tight group-hover:text-brand-orange whitespace-nowrap transition-opacity duration-150 ${c ? 'opacity-0' : 'opacity-100'}`}>
|
||||
<span className={`text-xl font-bold text-white tracking-tight group-hover:text-brand-orange whitespace-nowrap transition-opacity duration-150 ${c ? 'opacity-0' : 'opacity-100'}`}>
|
||||
Pulse
|
||||
</span>
|
||||
</Link>
|
||||
@@ -387,7 +387,7 @@ export default function Sidebar({
|
||||
</nav>
|
||||
|
||||
{/* Bottom — utility items */}
|
||||
<div className="border-t border-neutral-200/60 dark:border-neutral-800/60 px-2 py-3 shrink-0">
|
||||
<div className="border-t border-neutral-800/60 px-2 py-3 shrink-0">
|
||||
{/* Notifications, Profile — same layout as nav items */}
|
||||
<div className="space-y-0.5 mb-1">
|
||||
<span title={c ? 'Notifications' : undefined}>
|
||||
@@ -418,7 +418,7 @@ export default function Sidebar({
|
||||
{!isMobile && (
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 w-full overflow-hidden"
|
||||
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-400 dark:text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 w-full overflow-hidden"
|
||||
title={collapsed ? 'Expand sidebar (press [)' : 'Collapse sidebar (press [)'}
|
||||
>
|
||||
<span className="w-7 h-7 flex items-center justify-center shrink-0">
|
||||
@@ -437,7 +437,7 @@ export default function Sidebar({
|
||||
<>
|
||||
{/* Desktop — ssr:false means this only renders on client, no hydration flash */}
|
||||
<aside
|
||||
className="hidden md:flex flex-col shrink-0 border-r border-neutral-200/60 dark:border-neutral-800/60 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl overflow-hidden relative z-10"
|
||||
className="hidden md:flex flex-col shrink-0 border-r border-neutral-800/60 bg-neutral-900/90 backdrop-blur-xl overflow-hidden relative z-10"
|
||||
style={{ width: collapsed ? COLLAPSED : EXPANDED, transition: 'width 200ms cubic-bezier(0.4, 0, 0.2, 1)' }}
|
||||
>
|
||||
{sidebarContent(false)}
|
||||
@@ -447,10 +447,10 @@ export default function Sidebar({
|
||||
{mobileOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40 bg-black/30 md:hidden" onClick={onMobileClose} />
|
||||
<aside className="fixed inset-y-0 left-0 z-50 w-72 bg-white dark:bg-neutral-900 border-r border-neutral-200 dark:border-neutral-800 shadow-xl md:hidden animate-in slide-in-from-left duration-200">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-800">
|
||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">Navigation</span>
|
||||
<button onClick={onMobileClose} className="p-1.5 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300">
|
||||
<aside className="fixed inset-y-0 left-0 z-50 w-72 bg-neutral-900 border-r border-neutral-800 shadow-xl md:hidden animate-in slide-in-from-left duration-200">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-800">
|
||||
<span className="text-sm font-semibold text-white">Navigation</span>
|
||||
<button onClick={onMobileClose} className="p-1.5 text-neutral-400 hover:text-neutral-300">
|
||||
<XIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -131,17 +131,17 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
|
||||
return (
|
||||
<>
|
||||
<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="bg-neutral-900/80 border border-white/[0.08] rounded-2xl p-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<DeviceMobile className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Technology
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
className="p-1.5 text-neutral-400 dark:text-neutral-500 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-800 transition-all cursor-pointer rounded-lg"
|
||||
aria-label="View all technology"
|
||||
>
|
||||
<FrameCornersIcon className="w-4 h-4" weight="bold" />
|
||||
@@ -157,8 +157,8 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
aria-selected={activeTab === tab}
|
||||
className={`relative px-2.5 py-1 text-xs font-medium transition-colors capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
||||
activeTab === tab
|
||||
? 'text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
? 'text-white'
|
||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
@@ -177,7 +177,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
<div className="space-y-2 flex-1 min-h-[270px]">
|
||||
{isTabDisabled() ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-4">
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm">{getDisabledMessage()}</p>
|
||||
<p className="text-neutral-400 text-sm">{getDisabledMessage()}</p>
|
||||
</div>
|
||||
) : hasData ? (
|
||||
<>
|
||||
@@ -190,13 +190,13 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
<div
|
||||
key={item.name}
|
||||
onClick={() => canFilter && onFilter({ dimension: dim, operator: 'is', values: [item.name] })}
|
||||
className={`relative flex items-center justify-between h-9 group hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`relative flex items-center justify-between h-9 group hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
<div className="relative flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||
<div className="relative flex-1 truncate text-white flex items-center gap-3">
|
||||
{item.icon && <span className="text-lg">{item.icon}</span>}
|
||||
<span className="truncate">{capitalize(item.name)}</span>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
<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">
|
||||
{totalPageviews > 0 ? `${Math.round((item.pageviews / totalPageviews) * 100)}%` : ''}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(item.pageviews)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -217,13 +217,13 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GridIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-800 p-4">
|
||||
<GridIcon className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No technology data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
<p className="text-sm text-neutral-400 max-w-xs">
|
||||
Browser, OS, and device information will appear as visitors arrive.
|
||||
</p>
|
||||
<Link
|
||||
@@ -250,7 +250,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
value={modalSearch}
|
||||
onChange={(e) => setModalSearch(e.target.value)}
|
||||
placeholder="Search technology..."
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[80vh]">
|
||||
@@ -273,9 +273,9 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
<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 transition-colors${canFilter ? ' cursor-pointer' : ''}`}
|
||||
className={`flex items-center justify-between h-9 group hover:bg-neutral-800 rounded-lg px-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-white flex items-center gap-3">
|
||||
{item.icon && <span className="text-lg">{item.icon}</span>}
|
||||
<span className="truncate">{capitalize(item.name)}</span>
|
||||
</div>
|
||||
@@ -283,7 +283,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
<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">
|
||||
<span className="text-sm font-semibold text-neutral-400">
|
||||
{formatNumber(item.pageviews)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user