fix: Remove car_id property from adminAddVehicle call to fix TypeScript error
This commit is contained in:
424
temp_visitor_stats.tsx
Normal file
424
temp_visitor_stats.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user