feat: glass treatment + dark-only cleanup for dashboard components and navigation

This commit is contained in:
Usman Baig
2026-03-21 19:26:25 +01:00
parent 64b245caca
commit 7bf7e5cc3d
15 changed files with 166 additions and 166 deletions

View File

@@ -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 ${ className={`inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg transition-all cursor-pointer ${
isOpen isOpen
? 'bg-brand-orange/10 text-brand-orange border border-brand-orange/30' ? '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}> <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> </button>
{isOpen && ( {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 ? ( {!selectedDim ? (
/* Step 1: Dimension list */ /* Step 1: Dimension list */
<div className="py-1"> <div className="py-1">
@@ -129,9 +129,9 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
<button <button
key={dim} key={dim}
onClick={() => setSelectedDim(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}> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg> </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"> <div className="flex items-center gap-2 px-3 pt-3 pb-2">
<button <button
onClick={() => { setSelectedDim(null); setSearch(''); setOperator('is'); setFetchedSuggestions([]) }} 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}> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg> </svg>
</button> </button>
<span className="text-sm font-semibold text-neutral-900 dark:text-white"> <span className="text-sm font-semibold text-white">
{DIMENSION_LABELS[selectedDim]} {DIMENSION_LABELS[selectedDim]}
</span> </span>
</div> </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 ${ className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors cursor-pointer ${
operator === op operator === op
? 'bg-brand-orange text-white' ? '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]} {OPERATOR_LABELS[op]}
@@ -189,24 +189,24 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
} }
}} }}
placeholder={`Search ${DIMENSION_LABELS[selectedDim]?.toLowerCase()}...`} 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> </div>
{/* Values list */} {/* Values list */}
{isFetching ? ( {isFetching ? (
<div className="px-4 py-6 text-center"> <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> </div>
) : filtered.length > 0 ? ( ) : 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 => ( {filtered.map(s => (
<button <button
key={s.value} key={s.value}
onClick={() => handleSelectValue(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 && ( {s.count !== undefined && (
<span className="text-xs text-neutral-400 dark:text-neutral-500 ml-2 tabular-nums flex-shrink-0"> <span className="text-xs text-neutral-400 dark:text-neutral-500 ml-2 tabular-nums flex-shrink-0">
{s.count.toLocaleString()} {s.count.toLocaleString()}
@@ -216,7 +216,7 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg
))} ))}
</div> </div>
) : search.trim() ? ( ) : 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 <button
onClick={handleSubmitCustom} 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" 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"

View File

@@ -124,17 +124,17 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
return ( 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 justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Megaphone className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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 Campaigns
</h3> </h3>
{showViewAll && ( {showViewAll && (
<button <button
onClick={() => setIsModalOpen(true)} 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" aria-label="View all campaigns"
> >
<FrameCornersIcon className="w-4 h-4" weight="bold" /> <FrameCornersIcon className="w-4 h-4" weight="bold" />
@@ -161,13 +161,13 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
<div <div
key={`${item.source}|${item.medium}|${item.campaign}`} key={`${item.source}|${item.medium}|${item.campaign}`}
onClick={() => onFilter?.({ dimension: 'utm_source', operator: 'is', values: [item.source] })} 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 <div
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all" 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}%` }} 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)} {renderSourceIcon(item.source)}
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate font-medium text-sm" title={item.source}> <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"> <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)}%` : ''} {totalVisitors > 0 ? `${Math.round((item.visitors / totalVisitors) * 100)}%` : ''}
</span> </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)} {formatNumber(item.visitors)}
</span> </span>
</div> </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="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"> <div className="rounded-full bg-neutral-800 p-4">
<Megaphone className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <Megaphone className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
Track your marketing campaigns Track your marketing campaigns
</h4> </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. Add UTM parameters to your links to see campaign performance here.
</p> </p>
<button <button
@@ -230,7 +230,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
value={modalSearch} value={modalSearch}
onChange={(e) => setModalSearch(e.target.value)} onChange={(e) => setModalSearch(e.target.value)}
placeholder="Search campaigns..." 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>
<div className="max-h-[80vh]"> <div className="max-h-[80vh]">
@@ -262,12 +262,12 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
<div <div
key={`${item.source}|${item.medium}|${item.campaign}`} key={`${item.source}|${item.medium}|${item.campaign}`}
onClick={() => { if (onFilter) { onFilter({ dimension: 'utm_source', operator: 'is', values: [item.source] }); setIsModalOpen(false) } }} 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"> <div className="flex-1 flex items-center gap-3 min-w-0">
{renderSourceIcon(item.source)} {renderSourceIcon(item.source)}
<div className="min-w-0"> <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)} {getReferrerDisplayName(item.source)}
</div> </div>
<div className="flex items-center gap-1.5 text-[11px] text-neutral-400 dark:text-neutral-500"> <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"> <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)}%` : ''} {modalTotal > 0 ? `${Math.round((item.visitors / modalTotal) * 100)}%` : ''}
</span> </span>
<span className="font-semibold text-neutral-900 dark:text-white"> <span className="font-semibold text-white">
{formatNumber(item.visitors)} {formatNumber(item.visitors)}
</span> </span>
<span className="text-neutral-400 dark:text-neutral-500 w-16 text-right"> <span className="text-neutral-400 dark:text-neutral-500 w-16 text-right">

View File

@@ -8,10 +8,10 @@ export default function ContentHeader({
onMobileMenuOpen: () => void onMobileMenuOpen: () => void
}) { }) {
return ( 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 <button
onClick={onMobileMenuOpen} 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" aria-label="Open navigation"
> >
<MenuIcon className="w-5 h-5" /> <MenuIcon className="w-5 h-5" />

View File

@@ -99,17 +99,17 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
return ( 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 justify-between mb-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Files className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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 Pages
</h3> </h3>
{showViewAll && ( {showViewAll && (
<button <button
onClick={() => setIsModalOpen(true)} 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" aria-label="View all pages"
> >
<FrameCornersIcon className="w-4 h-4" weight="bold" /> <FrameCornersIcon className="w-4 h-4" weight="bold" />
@@ -125,8 +125,8 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
aria-selected={activeTab === tab} 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 ${ 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 activeTab === tab
? 'text-neutral-900 dark:text-white' ? 'text-white'
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' : 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
}`} }`}
> >
{getTabLabel(tab)} {getTabLabel(tab)}
@@ -145,7 +145,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
<div className="space-y-2 flex-1 min-h-[270px]"> <div className="space-y-2 flex-1 min-h-[270px]">
{!collectPagePaths ? ( {!collectPagePaths ? (
<div className="h-full flex flex-col items-center justify-center text-center px-4"> <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> </div>
) : hasData ? ( ) : hasData ? (
<> <>
@@ -156,13 +156,13 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
<div <div
key={page.path} key={page.path}
onClick={() => onFilter?.({ dimension: 'page', operator: 'is', values: [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 <div
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all" 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}%` }} 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> <span className="truncate">{page.path}</span>
<a <a
href={`https://${domain.replace(/^https?:\/\//, '')}${page.path}`} 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"> <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)}%` : ''} {totalPageviews > 0 ? `${Math.round((page.pageviews / totalPageviews) * 100)}%` : ''}
</span> </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)} {formatNumber(page.pageviews)}
</span> </span>
</div> </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="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"> <div className="rounded-full bg-neutral-800 p-4">
<LayoutDashboardIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <LayoutDashboardIcon className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
No page data yet No page data yet
</h4> </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. Your most visited pages will appear here as traffic arrives.
</p> </p>
<Link <Link
@@ -224,7 +224,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
value={modalSearch} value={modalSearch}
onChange={(e) => setModalSearch(e.target.value)} onChange={(e) => setModalSearch(e.target.value)}
placeholder="Search pages..." 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>
<div className="max-h-[80vh]"> <div className="max-h-[80vh]">
@@ -246,16 +246,16 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
<div <div
key={page.path} key={page.path}
onClick={() => { if (canFilter) { onFilter({ dimension: 'page', operator: 'is', values: [page.path] }); setIsModalOpen(false) } }} 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> <span className="truncate">{page.path}</span>
</div> </div>
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"> <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)}%` : ''} {modalTotal > 0 ? `${Math.round((page.pageviews / modalTotal) * 100)}%` : ''}
</span> </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)} {formatNumber(page.pageviews)}
</span> </span>
</div> </div>

View File

@@ -11,7 +11,7 @@ const Sidebar = dynamic(() => import('./Sidebar'), {
// so page content never occupies the sidebar zone // so page content never occupies the sidebar zone
loading: () => ( loading: () => (
<div <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 }} style={{ width: 64 }}
/> />
), ),

View File

@@ -150,7 +150,7 @@ export default function DottedMap({ data, className, formatValue = formatNumber
{tooltip && ( {tooltip && (
<div <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 }} style={{ left: tooltip.x, top: tooltip.y }}
> >
<span>{getCountryName(tooltip.country)}</span> <span>{getCountryName(tooltip.country)}</span>

View File

@@ -36,14 +36,14 @@ export default function EventProperties({ siteId, eventName, dateRange, onClose
const maxCount = values.length > 0 ? values[0].count : 1 const maxCount = values.length > 0 ? values[0].count : 1
return ( 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"> <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> Properties: <span className="text-brand-orange">{eventName.replace(/_/g, ' ')}</span>
</h3> </h3>
<button <button
onClick={onClose} 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}> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -54,11 +54,11 @@ export default function EventProperties({ siteId, eventName, dateRange, onClose
{loading ? ( {loading ? (
<div className="animate-pulse space-y-3"> <div className="animate-pulse space-y-3">
{[1, 2, 3].map(i => ( {[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> </div>
) : keys.length === 0 ? ( ) : 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. No properties recorded for this event yet.
</p> </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 ${ className={`px-3 py-1 text-xs font-medium rounded-full transition-colors cursor-pointer ${
selectedKey === k.key selectedKey === k.key
? 'bg-brand-orange text-white' ? '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} {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 key={v.value} className="flex items-center gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1"> <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} {v.value}
</span> </span>
<span className="text-xs font-semibold text-brand-orange tabular-nums ml-2"> <span className="text-xs font-semibold text-brand-orange tabular-nums ml-2">
{formatNumber(v.count)} {formatNumber(v.count)}
</span> </span>
</div> </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 <div
className="h-full bg-brand-orange/60 rounded-full transition-all" className="h-full bg-brand-orange/60 rounded-full transition-all"
style={{ width: `${(v.count / maxCount) * 100}%` }} style={{ width: `${(v.count / maxCount) * 100}%` }}

View File

@@ -20,11 +20,11 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
const emptySlots = Math.max(0, 6 - list.length) const emptySlots = Math.max(0, 6 - list.length)
return ( 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 justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Target className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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 Goals & Events
</h3> </h3>
</div> </div>
@@ -36,10 +36,10 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
<div <div
key={row.event_name} key={row.event_name}
onClick={() => onSelectEvent?.(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"> <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, ' ')} {row.display_name ?? row.event_name.replace(/_/g, ' ')}
</span> </span>
</div> </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"> <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)}%` : ''} {total > 0 ? `${Math.round((row.count / total) * 100)}%` : ''}
</span> </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)} {formatNumber(row.count)}
</span> </span>
</div> </div>
@@ -59,14 +59,14 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
</div> </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="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"> <div className="rounded-full bg-neutral-800 p-4">
<BookOpenIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <BookOpenIcon className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
Need help tracking goals? Need help tracking goals?
</h4> </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">
Add <code className="px-1.5 py-0.5 rounded bg-neutral-200 dark:bg-neutral-700 text-xs font-mono">pulse.track(&apos;event_name&apos;)</code> where actions happen on your site, then see counts here. Check our guide for step-by-step instructions. Add <code className="px-1.5 py-0.5 rounded bg-neutral-700 text-xs font-mono">pulse.track(&apos;event_name&apos;)</code> where actions happen on your site, then see counts here. Check our guide for step-by-step instructions.
</p> </p>
<Link <Link
href="/installation" href="/installation"

View File

@@ -90,13 +90,13 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
case 'T1': case 'T1':
return <ShieldCheck className="w-5 h-5 text-purple-600 dark:text-purple-400" /> return <ShieldCheck className="w-5 h-5 text-purple-600 dark:text-purple-400" />
case 'A1': 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': case 'A2':
return <Broadcast className="w-5 h-5 text-blue-500 dark:text-blue-400" /> return <Broadcast className="w-5 h-5 text-blue-500 dark:text-blue-400" />
case 'O1': case 'O1':
case 'EU': case 'EU':
case 'AP': 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] const FlagComponent = (Flags as Record<string, React.ComponentType<{ className?: string }>>)[countryCode]
@@ -216,17 +216,17 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
return ( 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 justify-between mb-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MapPin className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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 Locations
</h3> </h3>
{showViewAll && ( {showViewAll && (
<button <button
onClick={() => setIsModalOpen(true)} 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" aria-label="View all locations"
> >
<FrameCornersIcon className="w-4 h-4" weight="bold" /> <FrameCornersIcon className="w-4 h-4" weight="bold" />
@@ -242,8 +242,8 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
aria-selected={activeTab === tab} 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 ${ 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 activeTab === tab
? 'text-neutral-900 dark:text-white' ? 'text-white'
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' : 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
}`} }`}
> >
{tab} {tab}
@@ -262,20 +262,20 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<div className="space-y-2 flex-1 min-h-[270px]"> <div className="space-y-2 flex-1 min-h-[270px]">
{isTabDisabled() ? ( {isTabDisabled() ? (
<div className="h-full flex flex-col items-center justify-center text-center px-4"> <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> </div>
) : isVisualTab ? ( ) : isVisualTab ? (
hasData ? ( hasData ? (
inView ? <DottedMap data={filterUnknown(countries) as { country: string; pageviews: number }[]} /> : null 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="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"> <div className="rounded-full bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <GlobeIcon className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
No location data yet No location data yet
</h4> </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. Visitor locations will appear here based on anonymous geographic data.
</p> </p>
<Link <Link
@@ -300,13 +300,13 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<div <div
key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`} key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`}
onClick={() => canFilter && onFilter({ dimension: dim, operator: 'is', values: [filterValue!] })} 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 <div
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all" 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}%` }} 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="shrink-0">{getFlagComponent(item.country ?? '')}</span>
<span className="truncate"> <span className="truncate">
{activeTab === 'countries' ? getCountryName(item.country ?? '') : {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"> <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)}%` : ''} {totalPageviews > 0 ? `${Math.round((item.pageviews / totalPageviews) * 100)}%` : ''}
</span> </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)} {formatNumber(item.pageviews)}
</span> </span>
</div> </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="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"> <div className="rounded-full bg-neutral-800 p-4">
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <GlobeIcon className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
No location data yet No location data yet
</h4> </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. Visitor locations will appear here based on anonymous geographic data.
</p> </p>
</div> </div>
@@ -358,7 +358,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
value={modalSearch} value={modalSearch}
onChange={(e) => setModalSearch(e.target.value)} onChange={(e) => setModalSearch(e.target.value)}
placeholder="Search locations..." 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>
<div className="max-h-[80vh]"> <div className="max-h-[80vh]">
@@ -387,9 +387,9 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<div <div
key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`} key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`}
onClick={() => { if (canFilter) { onFilter({ dimension: dim, operator: 'is', values: [filterValue!] }); setIsModalOpen(false) } }} 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="shrink-0">{getFlagComponent(item.country ?? '')}</span>
<span className="truncate"> <span className="truncate">
{activeTab === 'countries' ? getCountryName(item.country ?? '') : {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"> <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)}%` : ''} {modalTotal > 0 ? `${Math.round((item.pageviews / modalTotal) * 100)}%` : ''}
</span> </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)} {formatNumber(item.pageviews)}
</span> </span>
</div> </div>

View File

@@ -131,14 +131,14 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
} }
return ( 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 justify-between mb-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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>
</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 When your visitors are most active
</p> </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"> <div className="flex-1 min-h-[270px] flex flex-col justify-center gap-1.5">
{Array.from({ length: 7 }).map((_, i) => ( {Array.from({ length: 7 }).map((_, i) => (
<div key={i} className="flex items-center gap-1"> <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="w-7 h-3 rounded bg-neutral-800 animate-pulse" />
<div className="flex-1 h-5 rounded bg-neutral-200 dark:bg-neutral-800 animate-pulse" /> <div className="flex-1 h-5 rounded bg-neutral-800 animate-pulse" />
</div> </div>
))} ))}
</div> </div>
@@ -174,7 +174,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
key={`${animKey}-${dayIdx}-${bucket}`} key={`${animKey}-${dayIdx}-${bucket}`}
className={[ className={[
'aspect-square w-full rounded-[4px] border cursor-default transition-transform duration-100', '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' : '', isActive ? 'animate-cell-highlight' : '',
isHoveredCell ? 'scale-110 z-10 relative' : '', isHoveredCell ? 'scale-110 z-10 relative' : '',
isBestCell && !isHoveredCell ? 'ring-1 ring-brand-orange/40' : '', 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]) => ( {Object.entries(BUCKET_LABELS).map(([b, label]) => (
<span <span
key={b} 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}%` }} style={{ left: `${(Number(b) / BUCKETS) * 100}%` }}
> >
{label} {label}
</span> </span>
))} ))}
<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%' }} style={{ left: '100%' }}
> >
24:00 24:00
@@ -222,7 +222,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
{HIGHLIGHT_COLORS.map((color, i) => ( {HIGHLIGHT_COLORS.map((color, i) => (
<div <div
key={i} 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 }} style={{ backgroundColor: color }}
/> />
))} ))}
@@ -245,7 +245,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
transform: 'translate(-50%, -100%)', 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"> <div className="font-semibold mb-1">
{DAYS[hovered.day]} {formatBucket(hovered.bucket)} {DAYS[hovered.day]} {formatBucket(hovered.bucket)}
</div> </div>
@@ -269,7 +269,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
initial={{ opacity: 0, y: 4 }} initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.6 }} 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{' '} Your busiest time is{' '}
<span className="text-brand-orange font-medium"> <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"> <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 No data available for this period
</p> </p>
</div> </div>

View File

@@ -9,15 +9,15 @@ interface RealtimeVisitorsProps {
export default function RealtimeVisitors({ count }: RealtimeVisitorsProps) { export default function RealtimeVisitors({ count }: RealtimeVisitorsProps) {
return ( return (
<div <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="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 Real-time Visitors
</div> </div>
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div> <div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
</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()} /> <AnimatedNumber value={count} format={(v) => v.toLocaleString()} />
</div> </div>
</div> </div>

View File

@@ -28,13 +28,13 @@ export default function ScrollDepth({ goalCounts, totalPageviews }: ScrollDepthP
})) }))
return ( 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 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 Scroll Depth
</h3> </h3>
</div> </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 % of visitors who scrolled this far
</p> </p>
@@ -73,13 +73,13 @@ export default function ScrollDepth({ goalCounts, totalPageviews }: ScrollDepthP
</div> </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="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"> <div className="rounded-full bg-neutral-800 p-4">
<BarChartIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <BarChartIcon className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
No scroll data yet No scroll data yet
</h4> </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. Scroll depth tracking is automatic data will appear here once visitors start scrolling on your pages.
</p> </p>
</div> </div>

View File

@@ -36,7 +36,7 @@ function ChangeArrow({ current, previous, invert = false }: { current: number; p
function getPositionBadgeClasses(position: number): string { 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 <= 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 <= 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' 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 ( 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 */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MagnifyingGlass className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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 && ( {showViewAll && (
<button <button
onClick={() => setIsModalOpen(true)} 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" aria-label="View all search data"
> >
<FrameCornersIcon className="w-4 h-4" weight="bold" /> <FrameCornersIcon className="w-4 h-4" weight="bold" />
@@ -125,8 +125,8 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
aria-selected={activeTab === tab} 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 ${ 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 activeTab === tab
? 'text-neutral-900 dark:text-white' ? 'text-white'
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' : 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
}`} }`}
> >
{getTabLabel(tab)} {getTabLabel(tab)}
@@ -145,9 +145,9 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
{isLoading ? ( {isLoading ? (
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
<div className="flex items-center gap-6"> <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-20 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-24 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> </div>
<div className="space-y-2 mt-4"> <div className="space-y-2 mt-4">
<ListSkeleton rows={LIMIT} /> <ListSkeleton rows={LIMIT} />
@@ -158,22 +158,22 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
{/* Inline stats row */} {/* Inline stats row */}
<div className="flex items-center gap-5 mb-4"> <div className="flex items-center gap-5 mb-4">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-xs text-neutral-500 dark:text-neutral-400">Clicks</span> <span className="text-xs text-neutral-400">Clicks</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-white"> <span className="text-sm font-semibold text-white">
{formatNumber(overview?.total_clicks ?? 0)} {formatNumber(overview?.total_clicks ?? 0)}
</span> </span>
<ChangeArrow current={overview?.total_clicks ?? 0} previous={overview?.prev_clicks ?? 0} /> <ChangeArrow current={overview?.total_clicks ?? 0} previous={overview?.prev_clicks ?? 0} />
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-xs text-neutral-500 dark:text-neutral-400">Impressions</span> <span className="text-xs text-neutral-400">Impressions</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-white"> <span className="text-sm font-semibold text-white">
{formatNumber(overview?.total_impressions ?? 0)} {formatNumber(overview?.total_impressions ?? 0)}
</span> </span>
<ChangeArrow current={overview?.total_impressions ?? 0} previous={overview?.prev_impressions ?? 0} /> <ChangeArrow current={overview?.total_impressions ?? 0} previous={overview?.prev_impressions ?? 0} />
</div> </div>
<div className="flex items-center gap-1.5"> <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-xs text-neutral-400">Avg Position</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-white"> <span className="text-sm font-semibold text-white">
{(overview?.avg_position ?? 0).toFixed(1)} {(overview?.avg_position ?? 0).toFixed(1)}
</span> </span>
<ChangeArrow current={overview?.avg_position ?? 0} previous={overview?.prev_avg_position ?? 0} invert /> <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 ( return (
<div <div
key={label} 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 <div
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all" 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}%` }} 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} {label}
</span> </span>
<div className="relative flex items-center gap-3 ml-4 shrink-0"> <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"> <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)}%` : ''} {totalImpressions > 0 ? `${Math.round((row.impressions / totalImpressions) * 100)}%` : ''}
</span> </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)} {formatNumber(row.clicks)}
</span> </span>
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getPositionBadgeClasses(row.position)}`}> <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} value={modalSearch}
onChange={(e) => setModalSearch(e.target.value)} onChange={(e) => setModalSearch(e.target.value)}
placeholder={`Search ${activeTab}...`} 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>
<div className="max-h-[80vh]"> <div className="max-h-[80vh]">
@@ -266,16 +266,16 @@ export default function SearchPerformance({ siteId, dateRange }: SearchPerforman
return ( return (
<div <div
key={label} 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} {label}
</span> </span>
<div className="flex items-center gap-3 ml-4"> <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"> <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)}%` : ''} {modalTotal > 0 ? `${Math.round((row.impressions / modalTotal) * 100)}%` : ''}
</span> </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)} {formatNumber(row.clicks)}
</span> </span>
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getPositionBadgeClasses(row.position)}`}> <span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getPositionBadgeClasses(row.position)}`}>

View File

@@ -162,12 +162,12 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
setOpen(!open) 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"> <span className="w-7 h-7 rounded-md bg-brand-orange/10 flex items-center justify-center shrink-0 overflow-hidden">
{faviconUrl && !faviconFailed ? ( {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 <img
src={faviconUrl} src={faviconUrl}
alt="" alt=""
@@ -187,14 +187,14 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps
</button> </button>
{open && ( {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"> <div className="p-2">
<input <input
type="text" type="text"
placeholder="Search sites..." placeholder="Search sites..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} 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 autoFocus
/> />
</div> </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 ${ className={`w-full flex items-center gap-2.5 px-4 py-2 text-sm text-left ${
site.id === siteId site.id === siteId
? 'bg-brand-orange/10 text-brand-orange font-medium' ? '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 <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>} {filtered.length === 0 && <p className="px-4 py-3 text-sm text-neutral-400">No sites found</p>}
</div> </div>
<div className="border-t border-neutral-200 dark:border-neutral-700 p-2"> <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-50 dark:hover:bg-neutral-800 rounded-lg"> <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" /> <PlusIcon className="w-4 h-4" />
Add new site Add new site
</Link> </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 ${ className={`flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium overflow-hidden ${
active active
? 'bg-brand-orange/10 text-brand-orange' ? '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"> <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"> <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" /> <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>
<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 Pulse
</span> </span>
</Link> </Link>
@@ -387,7 +387,7 @@ export default function Sidebar({
</nav> </nav>
{/* Bottom — utility items */} {/* 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 */} {/* Notifications, Profile — same layout as nav items */}
<div className="space-y-0.5 mb-1"> <div className="space-y-0.5 mb-1">
<span title={c ? 'Notifications' : undefined}> <span title={c ? 'Notifications' : undefined}>
@@ -418,7 +418,7 @@ export default function Sidebar({
{!isMobile && ( {!isMobile && (
<button <button
onClick={toggle} 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 [)'} title={collapsed ? 'Expand sidebar (press [)' : 'Collapse sidebar (press [)'}
> >
<span className="w-7 h-7 flex items-center justify-center shrink-0"> <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 */} {/* Desktop — ssr:false means this only renders on client, no hydration flash */}
<aside <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)' }} style={{ width: collapsed ? COLLAPSED : EXPANDED, transition: 'width 200ms cubic-bezier(0.4, 0, 0.2, 1)' }}
> >
{sidebarContent(false)} {sidebarContent(false)}
@@ -447,10 +447,10 @@ export default function Sidebar({
{mobileOpen && ( {mobileOpen && (
<> <>
<div className="fixed inset-0 z-40 bg-black/30 md:hidden" onClick={onMobileClose} /> <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"> <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-200 dark:border-neutral-800"> <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-800">
<span className="text-sm font-semibold text-neutral-900 dark:text-white">Navigation</span> <span className="text-sm font-semibold text-white">Navigation</span>
<button onClick={onMobileClose} className="p-1.5 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"> <button onClick={onMobileClose} className="p-1.5 text-neutral-400 hover:text-neutral-300">
<XIcon className="w-5 h-5" /> <XIcon className="w-5 h-5" />
</button> </button>
</div> </div>

View File

@@ -131,17 +131,17 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
return ( 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 justify-between mb-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DeviceMobile className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" /> <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 Technology
</h3> </h3>
{showViewAll && ( {showViewAll && (
<button <button
onClick={() => setIsModalOpen(true)} 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" aria-label="View all technology"
> >
<FrameCornersIcon className="w-4 h-4" weight="bold" /> <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} 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 ${ 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 activeTab === tab
? 'text-neutral-900 dark:text-white' ? 'text-white'
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' : 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
}`} }`}
> >
{tab} {tab}
@@ -177,7 +177,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
<div className="space-y-2 flex-1 min-h-[270px]"> <div className="space-y-2 flex-1 min-h-[270px]">
{isTabDisabled() ? ( {isTabDisabled() ? (
<div className="h-full flex flex-col items-center justify-center text-center px-4"> <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> </div>
) : hasData ? ( ) : hasData ? (
<> <>
@@ -190,13 +190,13 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
<div <div
key={item.name} key={item.name}
onClick={() => canFilter && onFilter({ dimension: dim, operator: 'is', values: [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 <div
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all" 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}%` }} 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>} {item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{capitalize(item.name)}</span> <span className="truncate">{capitalize(item.name)}</span>
</div> </div>
@@ -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"> <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)}%` : ''} {totalPageviews > 0 ? `${Math.round((item.pageviews / totalPageviews) * 100)}%` : ''}
</span> </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)} {formatNumber(item.pageviews)}
</span> </span>
</div> </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="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"> <div className="rounded-full bg-neutral-800 p-4">
<GridIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <GridIcon className="w-8 h-8 text-neutral-400" />
</div> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-white">
No technology data yet No technology data yet
</h4> </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. Browser, OS, and device information will appear as visitors arrive.
</p> </p>
<Link <Link
@@ -250,7 +250,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
value={modalSearch} value={modalSearch}
onChange={(e) => setModalSearch(e.target.value)} onChange={(e) => setModalSearch(e.target.value)}
placeholder="Search technology..." 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>
<div className="max-h-[80vh]"> <div className="max-h-[80vh]">
@@ -273,9 +273,9 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
<div <div
key={item.name} key={item.name}
onClick={() => { if (canFilter) { onFilter({ dimension: dim, operator: 'is', values: [item.name] }); setIsModalOpen(false) } }} 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>} {item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{capitalize(item.name)}</span> <span className="truncate">{capitalize(item.name)}</span>
</div> </div>
@@ -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"> <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)}%` : ''} {modalTotal > 0 ? `${Math.round((item.pageviews / modalTotal) * 100)}%` : ''}
</span> </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)} {formatNumber(item.pageviews)}
</span> </span>
</div> </div>