From 4b46bba883b032d213cea13e7c03d5e823fb89ff Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Thu, 19 Mar 2026 17:23:57 +0100 Subject: [PATCH 01/60] feat: redesign Search dashboard card to match Pulse design language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add proportional impression bars, color-coded position badges, animated Queries/Pages tabs, hover percentage reveals, and searchable expand modal — bringing Search to parity with other dashboard cards. --- CHANGELOG.md | 1 + components/dashboard/SearchPerformance.tsx | 342 +++++++++++++++------ 2 files changed, 255 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a68ccb..5a0cc3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Improved +- **Redesigned Search card on the dashboard.** The Search section of the dashboard has been completely refreshed to match the rest of Pulse. Search queries now show proportional bars so you can visually compare which queries get the most impressions. Hovering a row reveals the impression share percentage. Position badges are now color-coded — green for page 1 rankings, orange for page 2, and red for queries buried beyond page 5. You can switch between your top search queries and top pages using tabs, and expand the full list in a searchable popup without leaving the dashboard. - **Smaller, faster tracking script.** The tracking script is now about 20% smaller. Logic like page path cleaning, referrer filtering, error page detection, and input validation has been moved from your browser to the Pulse server. This means the script loads faster on every page, and Pulse can improve these features without needing you to update anything. - **Automatic 404 page detection.** Pulse now detects error pages (404 / "Page Not Found") automatically on the server by reading your page title — no extra setup needed. Previously this ran in the browser and couldn't be improved without updating the script. Now Pulse can recognize more error page patterns over time, including pages in other languages, without any changes on your end. - **Smarter bot filtering.** Pulse now catches more types of automated traffic that were slipping through — like headless browsers with default screen sizes, bot farms that rotate through different locations, and bots that fire duplicate events within milliseconds. Bot detection checks have also been moved from the tracking script to the server, making the script smaller and faster for real visitors. diff --git a/components/dashboard/SearchPerformance.tsx b/components/dashboard/SearchPerformance.tsx index 0938a6d..949dcd8 100644 --- a/components/dashboard/SearchPerformance.tsx +++ b/components/dashboard/SearchPerformance.tsx @@ -1,14 +1,26 @@ 'use client' -import Link from 'next/link' -import { MagnifyingGlass, CaretUp, CaretDown } from '@phosphor-icons/react' -import { useGSCStatus, useGSCOverview, useGSCTopQueries } from '@/lib/swr/dashboard' +import { useState, useEffect } from 'react' +import { motion } from 'framer-motion' +import { logger } from '@/lib/utils/logger' +import { formatNumber, Modal } from '@ciphera-net/ui' +import { MagnifyingGlass, CaretUp, CaretDown, FrameCornersIcon } from '@phosphor-icons/react' +import { useGSCStatus, useGSCOverview, useGSCTopQueries, useGSCTopPages } from '@/lib/swr/dashboard' +import { getGSCTopQueries, getGSCTopPages } from '@/lib/api/gsc' +import type { GSCDataRow } from '@/lib/api/gsc' +import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' +import { ListSkeleton } from '@/components/skeletons' +import VirtualList from './VirtualList' interface SearchPerformanceProps { siteId: string dateRange: { start: string; end: string } } +type Tab = 'queries' | 'pages' + +const LIMIT = 7 + function ChangeArrow({ current, previous, invert = false }: { current: number; previous: number; invert?: boolean }) { if (!previous || previous === 0) return null const improved = invert ? current < previous : current > previous @@ -21,109 +33,263 @@ 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' + return 'text-red-500 dark:text-red-400 bg-red-500/10 dark:bg-red-500/20' +} + export default function SearchPerformance({ siteId, dateRange }: SearchPerformanceProps) { + const [activeTab, setActiveTab] = useState('queries') + const handleTabKeyDown = useTabListKeyboard() + const [isModalOpen, setIsModalOpen] = useState(false) + const [modalSearch, setModalSearch] = useState('') + const [fullData, setFullData] = useState([]) + const [isLoadingFull, setIsLoadingFull] = useState(false) + const { data: gscStatus } = useGSCStatus(siteId) const { data: overview, isLoading: overviewLoading } = useGSCOverview(siteId, dateRange.start, dateRange.end) - const { data: queriesData, isLoading: queriesLoading } = useGSCTopQueries(siteId, dateRange.start, dateRange.end, 5, 0) + const { data: queriesData, isLoading: queriesLoading } = useGSCTopQueries(siteId, dateRange.start, dateRange.end, LIMIT, 0) + const { data: pagesData, isLoading: pagesLoading } = useGSCTopPages(siteId, dateRange.start, dateRange.end, LIMIT, 0) - // Don't render if GSC is not connected or no data + // Fetch full data when modal opens (matches ContentStats/TopReferrers pattern) + useEffect(() => { + if (isModalOpen) { + const fetchData = async () => { + setIsLoadingFull(true) + try { + if (activeTab === 'queries') { + const data = await getGSCTopQueries(siteId, dateRange.start, dateRange.end, 100, 0) + setFullData(data.queries ?? []) + } else { + const data = await getGSCTopPages(siteId, dateRange.start, dateRange.end, 100, 0) + setFullData(data.pages ?? []) + } + } catch (e) { + logger.error(e) + } finally { + setIsLoadingFull(false) + } + } + fetchData() + } else { + setFullData([]) + } + }, [isModalOpen, activeTab, siteId, dateRange]) + + // Don't render if GSC is not connected if (!gscStatus?.connected) return null - const isLoading = overviewLoading || queriesLoading + const isLoading = overviewLoading || queriesLoading || pagesLoading const queries = queriesData?.queries ?? [] + const pages = pagesData?.pages ?? [] const hasData = overview && (overview.total_clicks > 0 || overview.total_impressions > 0) // Hide panel entirely if loaded but no data if (!isLoading && !hasData) return null - return ( -
- {/* Header */} -
-
- -

- Search -

-
- - View all → - -
+ const data = activeTab === 'queries' ? queries : pages + const totalImpressions = data.reduce((sum, d) => sum + d.impressions, 0) + const displayedData = data.slice(0, LIMIT) + const emptySlots = Math.max(0, LIMIT - displayedData.length) + const showViewAll = data.length >= LIMIT - {isLoading ? ( - /* Loading skeleton */ -
-
-
-
-
+ const getLabel = (row: GSCDataRow) => activeTab === 'queries' ? row.query : row.page + const getTabLabel = (tab: Tab) => tab === 'queries' ? 'Queries' : 'Pages' + + return ( + <> +
+ {/* Header */} +
+
+ +

Search

+ {showViewAll && ( + + )}
-
- {Array.from({ length: 5 }).map((_, i) => ( -
+
+ {(['queries', 'pages'] as Tab[]).map((tab) => ( + ))}
- ) : ( - <> - {/* Inline stats row */} -
-
- Clicks - - {(overview?.total_clicks ?? 0).toLocaleString()} - - -
-
- Impressions - - {(overview?.total_impressions ?? 0).toLocaleString()} - - -
-
- Avg Position - - {(overview?.avg_position ?? 0).toFixed(1)} - - -
-
- {/* Top 5 queries list */} -
- {queries.length > 0 ? ( - queries.map((q) => ( -
- - {q.query} - -
- - {q.clicks.toLocaleString()} - - - {q.position.toFixed(1)} - -
-
- )) - ) : ( -
-

No search data yet

-
- )} + {isLoading ? ( +
+
+
+
+
+
+
+ +
- - )} -
+ ) : ( + <> + {/* Inline stats row */} +
+
+ Clicks + + {formatNumber(overview?.total_clicks ?? 0)} + + +
+
+ Impressions + + {formatNumber(overview?.total_impressions ?? 0)} + + +
+
+ Avg Position + + {(overview?.avg_position ?? 0).toFixed(1)} + + +
+
+ + {/* Data list */} +
+ {displayedData.length > 0 ? ( + <> + {displayedData.map((row) => { + const maxImpressions = displayedData[0]?.impressions ?? 0 + const barWidth = maxImpressions > 0 ? (row.impressions / maxImpressions) * 75 : 0 + const label = getLabel(row) + return ( +
+
+ + {label} + +
+ + {totalImpressions > 0 ? `${Math.round((row.impressions / totalImpressions) * 100)}%` : ''} + + + {formatNumber(row.clicks)} + + + {row.position.toFixed(1)} + +
+
+ ) + })} + {Array.from({ length: emptySlots }).map((_, i) => ( + + + )} +
+ + {/* Expand modal */} + { setIsModalOpen(false); setModalSearch('') }} + title={`Search ${getTabLabel(activeTab)}`} + className="max-w-2xl" + > +
+ 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" + /> +
+
+ {isLoadingFull ? ( +
+ +
+ ) : (() => { + const source = fullData.length > 0 ? fullData : data + const modalData = source.filter(row => { + if (!modalSearch) return true + return getLabel(row).toLowerCase().includes(modalSearch.toLowerCase()) + }) + const modalTotal = modalData.reduce((sum, r) => sum + r.impressions, 0) + return ( + { + const label = getLabel(row) + return ( +
+ + {label} + +
+ + {modalTotal > 0 ? `${Math.round((row.impressions / modalTotal) * 100)}%` : ''} + + + {formatNumber(row.clicks)} + + + {row.position.toFixed(1)} + +
+
+ ) + }} + /> + ) + })()} +
+
+ ) } -- 2.49.1 From 8fdb8c4a2fb0750eb54cb971067f51b62c1fc41c Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 18:17:27 +0100 Subject: [PATCH 02/60] fix: remove orange glow orb backgrounds from marketing pages --- app/about/page.tsx | 1 - app/features/page.tsx | 1 - app/installation/page.tsx | 2 -- app/integrations/nextjs/page.tsx | 1 - app/integrations/page.tsx | 1 - app/integrations/react/page.tsx | 1 - app/integrations/vue/page.tsx | 1 - app/integrations/wordpress/page.tsx | 1 - app/not-found.tsx | 2 -- app/page.tsx | 4 ---- components/IntegrationGuide.tsx | 1 - 11 files changed, 16 deletions(-) diff --git a/app/about/page.tsx b/app/about/page.tsx index f37e4af..4e4c649 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -60,7 +60,6 @@ export default function AboutPage() {
{/* * --- ATMOSPHERE (Background) --- */}
-
{/* * --- ATMOSPHERE (Background) --- */}
-
- {/* * Top-left Orange Glow */} -
{/* * Bottom-right Neutral Glow */}
{/* * Grid Pattern with Radial Mask */} diff --git a/app/integrations/nextjs/page.tsx b/app/integrations/nextjs/page.tsx index 2a26425..31f76b6 100644 --- a/app/integrations/nextjs/page.tsx +++ b/app/integrations/nextjs/page.tsx @@ -8,7 +8,6 @@ export default function NextJsIntegrationPage() {
{/* * --- ATMOSPHERE (Background) --- */}
-
{/* * --- ATMOSPHERE (Background) --- */}
-
{/* * --- ATMOSPHERE (Background) --- */}
-
{/* * --- ATMOSPHERE (Background) --- */}
-
{/* * --- ATMOSPHERE (Background) --- */}
-
{/* * --- ATMOSPHERE (Background) --- */}
- {/* * Center Orange Glow */} -
{/* * Grid Pattern with Radial Mask */}
-
- - {/* * Top-left Orange Glow */} -
{/* * Bottom-right Neutral Glow */}
{/* * Grid Pattern with Radial Mask */} diff --git a/components/IntegrationGuide.tsx b/components/IntegrationGuide.tsx index fb53861..c309f63 100644 --- a/components/IntegrationGuide.tsx +++ b/components/IntegrationGuide.tsx @@ -39,7 +39,6 @@ export function IntegrationGuide({ integration, children }: IntegrationGuideProp
{/* * --- ATMOSPHERE (Background) --- */}
-
Date: Sat, 21 Mar 2026 18:23:12 +0100 Subject: [PATCH 03/60] fix: remove orange radial gradient from body background --- styles/globals.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/styles/globals.css b/styles/globals.css index a37e520..0592456 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -56,11 +56,7 @@ } body { - @apply bg-ciphera-gradient bg-fixed; - } - - .dark body { - @apply bg-ciphera-gradient-dark; + @apply bg-white dark:bg-neutral-950; } } -- 2.49.1 From ec9d1a2c2df5c3d5eb06e668c5f82a7c72f02a03 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 18:27:35 +0100 Subject: [PATCH 04/60] feat: force dark mode and match ciphera-website background --- app/layout.tsx | 16 +++++++--------- styles/globals.css | 29 +++-------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 13c4f7b..cc9d960 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -import { ThemeProviders, Toaster } from '@ciphera-net/ui' +import { Toaster } from '@ciphera-net/ui' import { AuthProvider } from '@/lib/auth/context' import SWRProvider from '@/components/SWRProvider' import type { Metadata, Viewport } from 'next' @@ -45,15 +45,13 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - - + + - - - {children} - - - + + {children} + + diff --git a/styles/globals.css b/styles/globals.css index 0592456..315ee27 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -10,29 +10,6 @@ --color-error: #EF4444; /* * Chart colors */ - --chart-1: #FD5E0F; - --chart-2: #3b82f6; - --chart-3: #22c55e; - --chart-4: #a855f7; - --chart-5: #f59e0b; - --chart-grid: #f5f5f5; - --chart-axis: #a3a3a3; - - /* * shadcn-compatible semantic tokens (for 21st.dev components) */ - --background: 255 255 255; - --foreground: 23 23 23; - --card: 255 255 255; - --card-foreground: 23 23 23; - --popover: 255 255 255; - --popover-foreground: 23 23 23; - --primary: 253 94 15; - --primary-foreground: 255 255 255; - --secondary: 245 245 245; - --secondary-foreground: 23 23 23; - --destructive-foreground: 255 255 255; - } - - .dark { --chart-1: #FD5E0F; --chart-2: #60a5fa; --chart-3: #4ade80; @@ -41,7 +18,7 @@ --chart-grid: #262626; --chart-axis: #737373; - /* * shadcn-compatible dark mode overrides */ + /* * shadcn-compatible semantic tokens (dark-only) */ --background: 10 10 10; --foreground: 250 250 250; --card: 23 23 23; @@ -54,9 +31,9 @@ --secondary-foreground: 250 250 250; --destructive-foreground: 255 255 255; } - + body { - @apply bg-white dark:bg-neutral-950; + @apply bg-neutral-950 text-neutral-100 antialiased; } } -- 2.49.1 From 21c68b43344200564b84b73d227eff024a804d24 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 18:30:06 +0100 Subject: [PATCH 05/60] fix: restore ThemeProvider with forced dark mode to fix build --- app/layout.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index cc9d960..632089c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -import { Toaster } from '@ciphera-net/ui' +import { ThemeProvider, Toaster } from '@ciphera-net/ui' import { AuthProvider } from '@/lib/auth/context' import SWRProvider from '@/components/SWRProvider' import type { Metadata, Viewport } from 'next' @@ -48,10 +48,12 @@ export default function RootLayout({ - - {children} - - + + + {children} + + + -- 2.49.1 From 6d253e6d18a23a630126adb74082561d8a0cf316 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 18:54:15 +0100 Subject: [PATCH 06/60] chore: bump @ciphera-net/ui to 0.3.0 (dark-only) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 587ffc0..cf8c85f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.15.0-alpha", "dependencies": { - "@ciphera-net/ui": "^0.2.15", + "@ciphera-net/ui": "^0.3.0", "@ducanh2912/next-pwa": "^10.2.9", "@phosphor-icons/react": "^2.1.10", "@simplewebauthn/browser": "^13.2.2", @@ -1670,9 +1670,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.2.15", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.15/ec35ffe3be80cb5deca6a05bd2d36d636333a4a9", - "integrity": "sha512-Y2snU21OFbcarVq6QbSkW/pbL3BL9SePf8dBzC36zUvDp5TuhIU7E/21ydVGxGH6Ye6wKw2G1Qsv3xsnsumyPA==", + "version": "0.3.0", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.3.0/d1016cffd0c4402d130c7fa3112348d9b75f730e", + "integrity": "sha512-d0Vn2N2l/Bawgy6UKKY6Fz94i+a5A2WA2UsuHLsDfxaG52OxMqpmeHu4mGV3tvPAqH/5K2yZC4E4ne1Tkes2xg==", "dependencies": { "@phosphor-icons/react": "^2.1.10", "class-variance-authority": "^0.7.1", diff --git a/package.json b/package.json index c13b8aa..66c210c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:watch": "vitest" }, "dependencies": { - "@ciphera-net/ui": "^0.2.15", + "@ciphera-net/ui": "^0.3.0", "@ducanh2912/next-pwa": "^10.2.9", "@phosphor-icons/react": "^2.1.10", "@simplewebauthn/browser": "^13.2.2", -- 2.49.1 From 64b245caca5dd0fecd947c1bf16cf0a52c97224d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 19:13:00 +0100 Subject: [PATCH 07/60] feat: update card component to glass treatment --- components/ui/card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ui/card.tsx b/components/ui/card.tsx index fe06801..35989e9 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -26,7 +26,7 @@ const useCardContext = () => { const cardVariants = cva('flex flex-col items-stretch text-card-foreground rounded-xl', { variants: { variant: { - default: 'bg-card border border-border shadow-xs black/5', + default: 'bg-neutral-900/80 border border-white/[0.08] backdrop-blur-sm', accent: 'bg-muted shadow-xs p-1', }, }, -- 2.49.1 From 7bf7e5cc3da095fd0054f28dde1edbc206e77795 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 19:26:25 +0100 Subject: [PATCH 08/60] feat: glass treatment + dark-only cleanup for dashboard components and navigation --- components/dashboard/AddFilterDropdown.tsx | 26 ++++++------ components/dashboard/Campaigns.tsx | 28 ++++++------- components/dashboard/ContentHeader.tsx | 4 +- components/dashboard/ContentStats.tsx | 34 ++++++++-------- components/dashboard/DashboardShell.tsx | 2 +- components/dashboard/DottedMap.tsx | 2 +- components/dashboard/EventProperties.tsx | 16 ++++---- components/dashboard/GoalStats.tsx | 20 +++++----- components/dashboard/Locations.tsx | 46 +++++++++++----------- components/dashboard/PeakHours.tsx | 24 +++++------ components/dashboard/RealtimeVisitors.tsx | 6 +-- components/dashboard/ScrollDepth.tsx | 14 +++---- components/dashboard/SearchPerformance.tsx | 44 ++++++++++----------- components/dashboard/Sidebar.tsx | 32 +++++++-------- components/dashboard/TechSpecs.tsx | 34 ++++++++-------- 15 files changed, 166 insertions(+), 166 deletions(-) diff --git a/components/dashboard/AddFilterDropdown.tsx b/components/dashboard/AddFilterDropdown.tsx index 2ad4066..74c835c 100644 --- a/components/dashboard/AddFilterDropdown.tsx +++ b/components/dashboard/AddFilterDropdown.tsx @@ -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' }`} > @@ -121,7 +121,7 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg {isOpen && ( -
+
{!selectedDim ? ( /* Step 1: Dimension list */
@@ -129,9 +129,9 @@ export default function AddFilterDropdown({ onAdd, suggestions = {}, onFetchSugg - + {DIMENSION_LABELS[selectedDim]}
@@ -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" />
{/* Values list */} {isFetching ? (
-
+
) : filtered.length > 0 ? ( -
+
{filtered.map(s => (
) : search.trim() ? ( -
+
@@ -262,12 +262,12 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
{ 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' : ''}`} >
{renderSourceIcon(item.source)}
-
+
{getReferrerDisplayName(item.source)}
@@ -281,7 +281,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp {modalTotal > 0 ? `${Math.round((item.visitors / modalTotal) * 100)}%` : ''} - + {formatNumber(item.visitors)} diff --git a/components/dashboard/ContentHeader.tsx b/components/dashboard/ContentHeader.tsx index 7f67b84..417fc13 100644 --- a/components/dashboard/ContentHeader.tsx +++ b/components/dashboard/ContentHeader.tsx @@ -8,10 +8,10 @@ export default function ContentHeader({ onMobileMenuOpen: () => void }) { return ( -
+
diff --git a/components/dashboard/TechSpecs.tsx b/components/dashboard/TechSpecs.tsx index 6139ec9..f9f63ac 100644 --- a/components/dashboard/TechSpecs.tsx +++ b/components/dashboard/TechSpecs.tsx @@ -131,17 +131,17 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co return ( <> -
+
-

+

Technology

{showViewAll && (