| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- import * as React from 'react';
- import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Sector } from 'recharts';
- import { subDays, format } from 'date-fns';
- import { AnalyticsData } from '../types';
- import { useTranslation } from '../hooks/useI18n';
- const generateAllTimeData = () => {
- const data = [];
- for (let i = 60; i >= 0; i--) {
- const date = subDays(new Date(), i);
- data.push({
- date: format(date, 'MMM d'),
- views: 1000 + Math.floor(Math.random() * 800) + (60 - i) * 15,
- clicks: 600 + Math.floor(Math.random() * 500) + (60 - i) * 10,
- uniqueVisitors: 700 + Math.floor(Math.random() * 400) + (60 - i) * 12,
- });
- }
- return data;
- };
- const allTimeData = generateAllTimeData();
- const allTimeReferrers = [
- { source: 'Instagram', visits: 12450 },
- { source: 'Twitter / X', visits: 9870 },
- { source: 'Direct', visits: 8320 },
- { source: 'LinkedIn', visits: 6400 },
- { source: 'Facebook', visits: 4100 },
- ];
- const allTimeLocations = [
- { country: 'USA', visits: 22500 },
- { country: 'India', visits: 15300 },
- { country: 'UK', visits: 11800 },
- { country: 'Canada', visits: 8900 },
- { country: 'Germany', visits: 7200 },
- ];
- const allTimeDevices = [
- { name: 'Mobile', value: 38400 },
- { name: 'Desktop', value: 16600 },
- ];
- const COLORS = ['#10b981', '#4b5563'];
- interface PageAnalyticsProps {
- analyticsData: AnalyticsData;
- }
- const PageAnalytics: React.FC<PageAnalyticsProps> = ({ analyticsData }) => {
- const { t } = useTranslation();
- const [timeRange, setTimeRange] = React.useState<'7' | '28' | 'all'>('7');
- const [isDarkMode, setIsDarkMode] = React.useState(document.documentElement.classList.contains('dark'));
- React.useEffect(() => {
- const observer = new MutationObserver(() => {
- setIsDarkMode(document.documentElement.classList.contains('dark'));
- });
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
- return () => observer.disconnect();
- }, []);
- const chartColors = {
- grid: isDarkMode ? '#374151' : '#e5e7eb',
- text: isDarkMode ? '#9CA3AF' : '#6b7280',
- tooltipBg: isDarkMode ? '#1F2937' : '#FFFFFF',
- tooltipBorder: isDarkMode ? '#4B5563' : '#d1d5db',
- };
- const processedAnalytics = React.useMemo(() => {
- const range = timeRange === 'all' ? allTimeData.length : parseInt(timeRange);
- const chartData = allTimeData.slice(-range);
-
- const summary = chartData.reduce((acc, day) => {
- acc.views += day.views;
- acc.clicks += day.clicks;
- acc.uniqueVisitors += day.uniqueVisitors;
- return acc;
- }, { views: 0, clicks: 0, uniqueVisitors: 0 });
- const ctr = summary.views > 0 ? `${((summary.clicks / summary.views) * 100).toFixed(1)}%` : '0%';
-
- const multiplier = range / allTimeData.length;
- const topReferrers = allTimeReferrers.map(r => ({ ...r, visits: Math.round(r.visits * multiplier) })).sort((a,b) => b.visits - a.visits);
- const topLocations = allTimeLocations.map(l => ({ ...l, visits: Math.round(l.visits * multiplier) })).sort((a,b) => b.visits - a.visits);
- const deviceData = allTimeDevices.map(d => ({ ...d, value: Math.round(d.value * multiplier) }));
- return { chartData, summary, ctr, topReferrers, topLocations, deviceData };
- }, [timeRange]);
- const renderActiveShape = (props: any) => {
- const RADIAN = Math.PI / 180;
- const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value } = props;
- const sin = Math.sin(-RADIAN * midAngle);
- const cos = Math.cos(-RADIAN * midAngle);
- const sx = cx + (outerRadius + 10) * cos;
- const sy = cy + (outerRadius + 10) * sin;
- const mx = cx + (outerRadius + 30) * cos;
- const my = cy + (outerRadius + 30) * sin;
- const ex = mx + (cos >= 0 ? 1 : -1) * 22;
- const ey = my;
- const textAnchor = cos >= 0 ? 'start' : 'end';
- return (
- <g>
- <text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>{payload.name}</text>
- <Sector
- cx={cx}
- cy={cy}
- innerRadius={innerRadius}
- outerRadius={outerRadius}
- startAngle={startAngle}
- endAngle={endAngle}
- fill={fill}
- />
- <Sector
- cx={cx}
- cy={cy}
- startAngle={startAngle}
- endAngle={endAngle}
- innerRadius={outerRadius + 6}
- outerRadius={outerRadius + 10}
- fill={fill}
- />
- <path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={fill} fill="none" />
- <circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
- <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill={isDarkMode ? '#fff' : '#000'}>{`${value.toLocaleString()}`}</text>
- <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#9ca3af">{`(Rate ${(percent * 100).toFixed(2)}%)`}</text>
- </g>
- );
- };
- const [activeIndex, setActiveIndex] = React.useState(0);
- const onPieEnter = (_: any, index: number) => setActiveIndex(index);
- return (
- <div className="p-6">
- <div className="flex justify-between items-center mb-6">
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('analytics.page.title')}</h2>
- <div className="flex items-center gap-1 bg-gray-200 dark:bg-gray-800 p-1 rounded-lg">
- {(['7', '28', 'all'] as const).map(range => (
- <button key={range} onClick={() => setTimeRange(range)} className={`px-3 py-1 text-sm rounded-md ${timeRange === range ? 'bg-brand-primary text-white' : 'hover:bg-gray-300 dark:hover:bg-gray-700'}`}>
- {range === 'all' ? t('analytics.page.all_time') : t('analytics.page.last_days', { range })}
- </button>
- ))}
- </div>
- </div>
- <div className="space-y-6">
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
- <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.views')}</h3>
- <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.summary.views.toLocaleString()}</p>
- </div>
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
- <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.clicks')}</h3>
- <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.summary.clicks.toLocaleString()}</p>
- </div>
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
- <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.ctr')}</h3>
- <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.ctr}</p>
- </div>
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
- <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.unique_visitors')}</h3>
- <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.summary.uniqueVisitors.toLocaleString()}</p>
- </div>
- </div>
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
- <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.performance')}</h3>
- <ResponsiveContainer width="100%" height={350}>
- <LineChart data={processedAnalytics.chartData}>
- <CartesianGrid strokeDasharray="3 3" stroke={chartColors.grid} />
- <XAxis dataKey="date" stroke={chartColors.text} fontSize={12} />
- <YAxis stroke={chartColors.text} fontSize={12} />
- <Tooltip contentStyle={{ backgroundColor: chartColors.tooltipBg, border: `1px solid ${chartColors.tooltipBorder}` }} />
- <Legend wrapperStyle={{ color: chartColors.text } as any} />
- <Line type="monotone" dataKey="views" name={t('analytics.page.views')} stroke="#10b981" strokeWidth={2} activeDot={{ r: 8 }} dot={false} />
- <Line type="monotone" dataKey="clicks" name={t('analytics.page.clicks')} stroke="#3b82f6" strokeWidth={2} activeDot={{ r: 8 }} dot={false} />
- </LineChart>
- </ResponsiveContainer>
- </div>
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg lg:col-span-1">
- <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.device_breakdown')}</h3>
- <ResponsiveContainer width="100%" height={300}>
- <PieChart>
- <Pie
- // @ts-ignore The recharts Pie component's type definition is missing the 'activeIndex' prop.
- activeIndex={activeIndex}
- activeShape={renderActiveShape}
- data={processedAnalytics.deviceData}
- cx="50%"
- cy="50%"
- innerRadius={60}
- outerRadius={80}
- fill="#8884d8"
- dataKey="value"
- onMouseEnter={onPieEnter}
- >
- {processedAnalytics.deviceData.map((entry, index) => (
- <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
- ))}
- </Pie>
- </PieChart>
- </ResponsiveContainer>
- </div>
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg lg:col-span-2">
- <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.top_locations')}</h3>
- <ResponsiveContainer width="100%" height={300}>
- <BarChart data={processedAnalytics.topLocations} layout="vertical" margin={{ top: 5, right: 20, left: 20, bottom: 5 }}>
- <CartesianGrid strokeDasharray="3 3" stroke={chartColors.grid} horizontal={false} />
- <XAxis type="number" stroke={chartColors.text} fontSize={12} />
- <YAxis type="category" dataKey="country" stroke={chartColors.text} fontSize={12} width={80} />
- <Tooltip contentStyle={{ backgroundColor: chartColors.tooltipBg, border: `1px solid ${chartColors.tooltipBorder}` }} cursor={{fill: isDarkMode ? '#374151' : '#f3f4f6'}}/>
- <Bar dataKey="visits" fill="#10b981" />
- </BarChart>
- </ResponsiveContainer>
- </div>
- </div>
- <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
- <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.top_referrers')}</h3>
- <ResponsiveContainer width="100%" height={300}>
- <BarChart data={processedAnalytics.topReferrers} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
- <CartesianGrid strokeDasharray="3 3" stroke={chartColors.grid} vertical={false} />
- <XAxis dataKey="source" stroke={chartColors.text} fontSize={12}/>
- <YAxis stroke={chartColors.text} fontSize={12}/>
- <Tooltip contentStyle={{ backgroundColor: chartColors.tooltipBg, border: `1px solid ${chartColors.tooltipBorder}` }} cursor={{fill: isDarkMode ? '#374151' : '#f3f4f6'}}/>
- <Bar dataKey="visits" fill="#10b981" />
- </BarChart>
- </ResponsiveContainer>
- </div>
- </div>
- </div>
- );
- };
- export default PageAnalytics;
|