Files
AutonetSellCar/temp_visitor_stats.tsx

425 lines
16 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { useAuth } from "@/contexts/AuthContext";
import {
visitorsApi,
OverviewStats,
ChartData,
BreakdownData,
TopPagesData,
TopReferrersData,
RealtimeStats,
CountryStats,
} from "@/lib/api";
import {
Users,
Globe,
Monitor,
Smartphone,
Tablet,
TrendingUp,
TrendingDown,
RefreshCw,
BarChart3,
Activity,
} from "lucide-react";
export default function VisitorStatsPage() {
const { user, token } = useAuth();
const [days, setDays] = useState(30);
const [loading, setLoading] = useState(true);
const [overview, setOverview] = useState<OverviewStats | null>(null);
const [visitsChart, setVisitsChart] = useState<ChartData | null>(null);
const [uniqueChart, setUniqueChart] = useState<ChartData | null>(null);
const [deviceBreakdown, setDeviceBreakdown] = useState<BreakdownData | null>(null);
const [browserBreakdown, setBrowserBreakdown] = useState<BreakdownData | null>(null);
const [osBreakdown, setOsBreakdown] = useState<BreakdownData | null>(null);
const [topPages, setTopPages] = useState<TopPagesData | null>(null);
const [topReferrers, setTopReferrers] = useState<TopReferrersData | null>(null);
const [realtime, setRealtime] = useState<RealtimeStats | null>(null);
const [countryStats, setCountryStats] = useState<CountryStats[]>([]);
const fetchData = useCallback(async () => {
if (!token) return;
setLoading(true);
try {
const [
overviewRes,
visitsRes,
uniqueRes,
deviceRes,
browserRes,
osRes,
pagesRes,
referrersRes,
realtimeRes,
countryRes,
] = await Promise.all([
visitorsApi.adminGetOverview(token, days),
visitorsApi.adminGetVisitsChart(token, days),
visitorsApi.adminGetUniqueChart(token, days),
visitorsApi.adminGetDeviceBreakdown(token, days),
visitorsApi.adminGetBrowserBreakdown(token, days),
visitorsApi.adminGetOsBreakdown(token, days),
visitorsApi.adminGetTopPages(token, days),
visitorsApi.adminGetTopReferrers(token, days),
visitorsApi.adminGetRealtime(token),
visitorsApi.adminGetCountryStats(token),
]);
setOverview(overviewRes);
setVisitsChart(visitsRes);
setUniqueChart(uniqueRes);
setDeviceBreakdown(deviceRes);
setBrowserBreakdown(browserRes);
setOsBreakdown(osRes);
setTopPages(pagesRes);
setTopReferrers(referrersRes);
setRealtime(realtimeRes);
setCountryStats(countryRes);
} catch (error) {
console.error("Failed to fetch stats:", error);
} finally {
setLoading(false);
}
}, [token, days]);
useEffect(() => {
fetchData();
}, [fetchData]);
// Auto-refresh realtime stats every 30 seconds
useEffect(() => {
if (!token) return;
const interval = setInterval(async () => {
try {
const realtimeRes = await visitorsApi.adminGetRealtime(token);
setRealtime(realtimeRes);
} catch (error) {
console.error("Failed to refresh realtime:", error);
}
}, 30000);
return () => clearInterval(interval);
}, [token]);
if (!user?.is_admin) {
return (
<div className="p-6 text-center text-red-500">
Access denied. Admin only.
</div>
);
}
const getCountryFlag = (countryCode: string) => {
const flags: Record<string, string> = {
KR: "🇰🇷", US: "🇺🇸", JP: "🇯🇵", CN: "🇨🇳", DE: "🇩🇪",
GB: "🇬🇧", FR: "🇫🇷", CA: "🇨🇦", AU: "🇦🇺", IN: "🇮🇳",
RU: "🇷🇺", BR: "🇧🇷", MX: "🇲🇽", ES: "🇪🇸", IT: "🇮🇹",
LO: "🏠", "??": "🌍",
};
return flags[countryCode] || "🌍";
};
const getDeviceIcon = (deviceType: string) => {
switch (deviceType.toLowerCase()) {
case "mobile": return <Smartphone className="w-4 h-4" />;
case "tablet": return <Tablet className="w-4 h-4" />;
default: return <Monitor className="w-4 h-4" />;
}
};
// Simple bar chart component
const BarChart = ({ data, labels, color = "bg-blue-500" }: { data: number[], labels: string[], color?: string }) => {
const max = Math.max(...data, 1);
return (
<div className="flex items-end gap-1 h-40">
{data.map((value, index) => (
<div key={index} className="flex-1 flex flex-col items-center gap-1">
<div
className={`w-full ${color} rounded-t transition-all`}
style={{ height: `${(value / max) * 100}%`, minHeight: value > 0 ? "4px" : "0" }}
title={`${labels[index]}: ${value}`}
/>
{data.length <= 14 && (
<span className="text-xs text-gray-500 -rotate-45 origin-top-left whitespace-nowrap">
{labels[index]}
</span>
)}
</div>
))}
</div>
);
};
// Progress bar component
const ProgressBar = ({ percentage, color = "bg-blue-500" }: { percentage: number, color?: string }) => (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className={`h-2.5 rounded-full ${color}`} style={{ width: `${percentage}%` }} />
</div>
);
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">Visitor Statistics</h1>
<p className="text-gray-500">Detailed analytics for your website</p>
</div>
<div className="flex items-center gap-4">
{/* Period selector */}
<div className="flex bg-gray-100 rounded-lg p-1">
{[7, 14, 30, 90].map((d) => (
<button
key={d}
onClick={() => setDays(d)}
className={`px-3 py-1 rounded-md text-sm font-medium transition ${
days === d
? "bg-white text-blue-600 shadow"
: "text-gray-600 hover:text-gray-900"
}`}
>
{d}d
</button>
))}
</div>
{/* Realtime indicator */}
<div className="flex items-center gap-2 bg-green-50 text-green-700 px-3 py-1 rounded-full">
<Activity className="w-4 h-4 animate-pulse" />
<span className="text-sm font-medium">
{realtime?.active_visitors || 0} active
</span>
</div>
{/* Refresh button */}
<button
onClick={fetchData}
disabled={loading}
className="p-2 rounded-lg hover:bg-gray-100 transition"
>
<RefreshCw className={`w-5 h-5 text-gray-600 ${loading ? "animate-spin" : ""}`} />
</button>
</div>
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Total Visits</p>
<p className="text-3xl font-bold text-gray-900">
{overview?.total_visits?.toLocaleString() || 0}
</p>
</div>
<div className="p-3 bg-blue-100 rounded-full">
<BarChart3 className="w-6 h-6 text-blue-600" />
</div>
</div>
<p className="text-sm text-gray-500 mt-2">Last {days} days</p>
</div>
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Unique Visitors</p>
<p className="text-3xl font-bold text-gray-900">
{overview?.unique_visitors?.toLocaleString() || 0}
</p>
</div>
<div className="p-3 bg-green-100 rounded-full">
<Users className="w-6 h-6 text-green-600" />
</div>
</div>
<p className="text-sm text-gray-500 mt-2">
{overview?.pages_per_visit || 0} pages/visit
</p>
</div>
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Today</p>
<p className="text-3xl font-bold text-gray-900">
{overview?.today_visitors || 0}
</p>
</div>
<div className={`p-3 rounded-full ${
(overview?.growth_rate || 0) >= 0 ? "bg-green-100" : "bg-red-100"
}`}>
{(overview?.growth_rate || 0) >= 0 ? (
<TrendingUp className="w-6 h-6 text-green-600" />
) : (
<TrendingDown className="w-6 h-6 text-red-600" />
)}
</div>
</div>
<p className={`text-sm mt-2 ${
(overview?.growth_rate || 0) >= 0 ? "text-green-600" : "text-red-600"
}`}>
{(overview?.growth_rate || 0) >= 0 ? "+" : ""}{overview?.growth_rate || 0}% vs yesterday
</p>
</div>
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Mobile</p>
<p className="text-3xl font-bold text-gray-900">
{overview?.mobile_percentage || 0}%
</p>
</div>
<div className="p-3 bg-purple-100 rounded-full">
<Smartphone className="w-6 h-6 text-purple-600" />
</div>
</div>
<p className="text-sm text-gray-500 mt-2">of all visitors</p>
</div>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Daily Visits Chart */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Daily Visits</h3>
{visitsChart && (
<BarChart data={visitsChart.data} labels={visitsChart.labels} color="bg-blue-500" />
)}
</div>
{/* Unique Visitors Chart */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Unique Visitors</h3>
{uniqueChart && (
<BarChart data={uniqueChart.data} labels={uniqueChart.labels} color="bg-green-500" />
)}
</div>
</div>
{/* Country Stats */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Globe className="w-5 h-5" />
Visitors by Country
</h3>
<div className="space-y-3">
{countryStats.map((stat, index) => {
const total = countryStats.reduce((sum, s) => sum + s.visitor_count, 0);
const percentage = total > 0 ? (stat.visitor_count / total) * 100 : 0;
return (
<div key={index} className="flex items-center gap-3">
<span className="text-xl">{getCountryFlag(stat.country_code)}</span>
<span className="w-32 font-medium text-gray-700">{stat.country}</span>
<div className="flex-1">
<ProgressBar percentage={percentage} color="bg-blue-500" />
</div>
<span className="w-16 text-right text-gray-600">
{stat.visitor_count.toLocaleString()}
</span>
<span className="w-16 text-right text-gray-400">
({percentage.toFixed(1)}%)
</span>
</div>
);
})}
{countryStats.length === 0 && (
<p className="text-gray-500 text-center py-4">No country data available</p>
)}
</div>
</div>
{/* Breakdowns Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Device Breakdown */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Device Type</h3>
<div className="space-y-3">
{deviceBreakdown?.items.map((item, index) => (
<div key={index} className="flex items-center gap-3">
{getDeviceIcon(item.name)}
<span className="flex-1 text-gray-700 capitalize">{item.name}</span>
<span className="text-gray-600">{item.count}</span>
<span className="text-gray-400 w-12 text-right">({item.percentage}%)</span>
</div>
))}
{(!deviceBreakdown?.items || deviceBreakdown.items.length === 0) && (
<p className="text-gray-500 text-center py-2">No data</p>
)}
</div>
</div>
{/* Browser Breakdown */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Browser</h3>
<div className="space-y-3">
{browserBreakdown?.items.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<span className="flex-1 text-gray-700">{item.name}</span>
<span className="text-gray-600">{item.count}</span>
<span className="text-gray-400 w-12 text-right">({item.percentage}%)</span>
</div>
))}
{(!browserBreakdown?.items || browserBreakdown.items.length === 0) && (
<p className="text-gray-500 text-center py-2">No data</p>
)}
</div>
</div>
{/* OS Breakdown */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Operating System</h3>
<div className="space-y-3">
{osBreakdown?.items.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<span className="flex-1 text-gray-700">{item.name}</span>
<span className="text-gray-600">{item.count}</span>
<span className="text-gray-400 w-12 text-right">({item.percentage}%)</span>
</div>
))}
{(!osBreakdown?.items || osBreakdown.items.length === 0) && (
<p className="text-gray-500 text-center py-2">No data</p>
)}
</div>
</div>
</div>
{/* Top Pages & Referrers */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Pages */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Top Pages</h3>
<div className="space-y-3">
{topPages?.pages.map((page, index) => (
<div key={index} className="flex items-center gap-3">
<span className="w-6 text-gray-400">{index + 1}.</span>
<span className="flex-1 text-gray-700 truncate font-mono text-sm">{page.path}</span>
<span className="text-gray-600">{page.count}</span>
</div>
))}
{(!topPages?.pages || topPages.pages.length === 0) && (
<p className="text-gray-500 text-center py-4">No page data available</p>
)}
</div>
</div>
{/* Top Referrers */}
<div className="bg-white rounded-xl shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Top Referrers</h3>
<div className="space-y-3">
{topReferrers?.referrers.map((ref, index) => (
<div key={index} className="flex items-center gap-3">
<span className="w-6 text-gray-400">{index + 1}.</span>
<span className="flex-1 text-gray-700 truncate">{ref.domain}</span>
<span className="text-gray-600">{ref.count}</span>
</div>
))}
{(!topReferrers?.referrers || topReferrers.referrers.length === 0) && (
<p className="text-gray-500 text-center py-4">No referrer data available</p>
)}
</div>
</div>
</div>
</div>
);
}