Phase 4: Badge & Empty State Improvements
This commit is contained in:
@@ -6,7 +6,7 @@ import { getSite, type Site } from '@/lib/api/sites'
|
||||
import { getRealtimeVisitors, getSessionDetails, type Visitor, type SessionEvent } from '@/lib/api/realtime'
|
||||
import { toast } from '@ciphera-net/ui'
|
||||
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
|
||||
import { LoadingOverlay } from '@ciphera-net/ui'
|
||||
import { LoadingOverlay, UserIcon } from '@ciphera-net/ui'
|
||||
|
||||
function formatTimeAgo(dateString: string) {
|
||||
const date = new Date(dateString)
|
||||
@@ -122,8 +122,16 @@ export default function RealtimePage() {
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{visitors.length === 0 ? (
|
||||
<div className="p-8 text-center text-neutral-500">
|
||||
No active visitors right now.
|
||||
<div className="p-8 flex flex-col items-center justify-center text-center gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-3">
|
||||
<UserIcon className="w-6 h-6 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-neutral-900 dark:text-white">
|
||||
No active visitors right now
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
New visitors will appear here in real-time
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||
|
||||
@@ -285,9 +285,9 @@ export default function PricingSection() {
|
||||
{isTeam && (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-brand-orange" />
|
||||
<div className="absolute top-4 right-4 bg-brand-orange/10 text-brand-orange text-[10px] font-bold px-2 py-1 rounded-full uppercase tracking-wide">
|
||||
<span className="absolute top-4 right-4 badge-primary">
|
||||
Most Popular
|
||||
</div>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
||||
import { formatNumber } from '@/lib/utils/format'
|
||||
import * as Flags from 'country-flag-icons/react/3x2'
|
||||
import WorldMap from './WorldMap'
|
||||
import { GlobeIcon } from '@ciphera-net/ui'
|
||||
|
||||
interface LocationProps {
|
||||
countries: Array<{ country: string; pageviews: number }>
|
||||
@@ -36,7 +37,19 @@ export default function Locations({ countries, cities }: LocationProps) {
|
||||
const renderContent = () => {
|
||||
if (activeTab === 'countries') {
|
||||
if (!countries || countries.length === 0) {
|
||||
return <p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No location data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
Visitor locations will appear here based on anonymous geographic data.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -60,7 +73,19 @@ export default function Locations({ countries, cities }: LocationProps) {
|
||||
|
||||
if (activeTab === 'cities') {
|
||||
if (!cities || cities.length === 0) {
|
||||
return <p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No city data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
City-level visitor data will appear as traffic grows.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -226,8 +226,16 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
</div>
|
||||
) : activeTab === 'map' ? (
|
||||
hasData ? <WorldMap data={filterUnknown(countries)} /> : (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No location data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
Visitor locations will appear here based on anonymous geographic data.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -252,14 +260,22 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
))}
|
||||
{Array.from({ length: emptySlots }).map((_, i) => (
|
||||
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No location data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
Visitor locations will appear here based on anonymous geographic data.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import { formatNumber } from '@/lib/utils/format'
|
||||
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
|
||||
import { MdMonitor } from 'react-icons/md'
|
||||
import { Modal } from '@ciphera-net/ui'
|
||||
import { Modal, GridIcon } from '@ciphera-net/ui'
|
||||
import { getBrowsers, getOS, getDevices, getScreenResolutions } from '@/lib/api/stats'
|
||||
|
||||
interface TechSpecsProps {
|
||||
@@ -165,8 +165,16 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GridIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No technology data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
Browser, OS, and device information will appear as visitors arrive.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { formatNumber } from '@/lib/utils/format'
|
||||
import { LayoutDashboardIcon } from '@ciphera-net/ui'
|
||||
|
||||
interface TopPagesProps {
|
||||
pages: Array<{ path: string; pageviews: number }>
|
||||
@@ -9,11 +10,21 @@ interface TopPagesProps {
|
||||
export default function TopPages({ pages }: TopPagesProps) {
|
||||
if (!pages || pages.length === 0) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 flex flex-col">
|
||||
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
|
||||
Top Pages
|
||||
</h3>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<LayoutDashboardIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No page data yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
Your most visited pages will appear here as traffic arrives.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { formatNumber } from '@/lib/utils/format'
|
||||
import { getReferrerIcon } from '@/lib/utils/icons'
|
||||
import { Modal } from '@ciphera-net/ui'
|
||||
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
||||
import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
|
||||
|
||||
interface TopReferrersProps {
|
||||
@@ -93,8 +93,16 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-600 dark:text-neutral-400">No data available</p>
|
||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
No referrers yet
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
||||
Traffic sources will appear here when visitors come from external sites.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user