PageAnalytics.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import * as React from 'react';
  2. import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Sector } from 'recharts';
  3. import { subDays, format } from 'date-fns';
  4. import { AnalyticsData } from '../types';
  5. import { useTranslation } from '../hooks/useI18n';
  6. const generateAllTimeData = () => {
  7. const data = [];
  8. for (let i = 60; i >= 0; i--) {
  9. const date = subDays(new Date(), i);
  10. data.push({
  11. date: format(date, 'MMM d'),
  12. views: 1000 + Math.floor(Math.random() * 800) + (60 - i) * 15,
  13. clicks: 600 + Math.floor(Math.random() * 500) + (60 - i) * 10,
  14. uniqueVisitors: 700 + Math.floor(Math.random() * 400) + (60 - i) * 12,
  15. });
  16. }
  17. return data;
  18. };
  19. const allTimeData = generateAllTimeData();
  20. const allTimeReferrers = [
  21. { source: 'Instagram', visits: 12450 },
  22. { source: 'Twitter / X', visits: 9870 },
  23. { source: 'Direct', visits: 8320 },
  24. { source: 'LinkedIn', visits: 6400 },
  25. { source: 'Facebook', visits: 4100 },
  26. ];
  27. const allTimeLocations = [
  28. { country: 'USA', visits: 22500 },
  29. { country: 'India', visits: 15300 },
  30. { country: 'UK', visits: 11800 },
  31. { country: 'Canada', visits: 8900 },
  32. { country: 'Germany', visits: 7200 },
  33. ];
  34. const allTimeDevices = [
  35. { name: 'Mobile', value: 38400 },
  36. { name: 'Desktop', value: 16600 },
  37. ];
  38. const COLORS = ['#10b981', '#4b5563'];
  39. interface PageAnalyticsProps {
  40. analyticsData: AnalyticsData;
  41. }
  42. const PageAnalytics: React.FC<PageAnalyticsProps> = ({ analyticsData }) => {
  43. const { t } = useTranslation();
  44. const [timeRange, setTimeRange] = React.useState<'7' | '28' | 'all'>('7');
  45. const [isDarkMode, setIsDarkMode] = React.useState(document.documentElement.classList.contains('dark'));
  46. React.useEffect(() => {
  47. const observer = new MutationObserver(() => {
  48. setIsDarkMode(document.documentElement.classList.contains('dark'));
  49. });
  50. observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
  51. return () => observer.disconnect();
  52. }, []);
  53. const chartColors = {
  54. grid: isDarkMode ? '#374151' : '#e5e7eb',
  55. text: isDarkMode ? '#9CA3AF' : '#6b7280',
  56. tooltipBg: isDarkMode ? '#1F2937' : '#FFFFFF',
  57. tooltipBorder: isDarkMode ? '#4B5563' : '#d1d5db',
  58. };
  59. const processedAnalytics = React.useMemo(() => {
  60. const range = timeRange === 'all' ? allTimeData.length : parseInt(timeRange);
  61. const chartData = allTimeData.slice(-range);
  62. const summary = chartData.reduce((acc, day) => {
  63. acc.views += day.views;
  64. acc.clicks += day.clicks;
  65. acc.uniqueVisitors += day.uniqueVisitors;
  66. return acc;
  67. }, { views: 0, clicks: 0, uniqueVisitors: 0 });
  68. const ctr = summary.views > 0 ? `${((summary.clicks / summary.views) * 100).toFixed(1)}%` : '0%';
  69. const multiplier = range / allTimeData.length;
  70. const topReferrers = allTimeReferrers.map(r => ({ ...r, visits: Math.round(r.visits * multiplier) })).sort((a,b) => b.visits - a.visits);
  71. const topLocations = allTimeLocations.map(l => ({ ...l, visits: Math.round(l.visits * multiplier) })).sort((a,b) => b.visits - a.visits);
  72. const deviceData = allTimeDevices.map(d => ({ ...d, value: Math.round(d.value * multiplier) }));
  73. return { chartData, summary, ctr, topReferrers, topLocations, deviceData };
  74. }, [timeRange]);
  75. const renderActiveShape = (props: any) => {
  76. const RADIAN = Math.PI / 180;
  77. const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value } = props;
  78. const sin = Math.sin(-RADIAN * midAngle);
  79. const cos = Math.cos(-RADIAN * midAngle);
  80. const sx = cx + (outerRadius + 10) * cos;
  81. const sy = cy + (outerRadius + 10) * sin;
  82. const mx = cx + (outerRadius + 30) * cos;
  83. const my = cy + (outerRadius + 30) * sin;
  84. const ex = mx + (cos >= 0 ? 1 : -1) * 22;
  85. const ey = my;
  86. const textAnchor = cos >= 0 ? 'start' : 'end';
  87. return (
  88. <g>
  89. <text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>{payload.name}</text>
  90. <Sector
  91. cx={cx}
  92. cy={cy}
  93. innerRadius={innerRadius}
  94. outerRadius={outerRadius}
  95. startAngle={startAngle}
  96. endAngle={endAngle}
  97. fill={fill}
  98. />
  99. <Sector
  100. cx={cx}
  101. cy={cy}
  102. startAngle={startAngle}
  103. endAngle={endAngle}
  104. innerRadius={outerRadius + 6}
  105. outerRadius={outerRadius + 10}
  106. fill={fill}
  107. />
  108. <path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={fill} fill="none" />
  109. <circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
  110. <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill={isDarkMode ? '#fff' : '#000'}>{`${value.toLocaleString()}`}</text>
  111. <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#9ca3af">{`(Rate ${(percent * 100).toFixed(2)}%)`}</text>
  112. </g>
  113. );
  114. };
  115. const [activeIndex, setActiveIndex] = React.useState(0);
  116. const onPieEnter = (_: any, index: number) => setActiveIndex(index);
  117. return (
  118. <div className="p-6">
  119. <div className="flex justify-between items-center mb-6">
  120. <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('analytics.page.title')}</h2>
  121. <div className="flex items-center gap-1 bg-gray-200 dark:bg-gray-800 p-1 rounded-lg">
  122. {(['7', '28', 'all'] as const).map(range => (
  123. <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'}`}>
  124. {range === 'all' ? t('analytics.page.all_time') : t('analytics.page.last_days', { range })}
  125. </button>
  126. ))}
  127. </div>
  128. </div>
  129. <div className="space-y-6">
  130. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
  131. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
  132. <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.views')}</h3>
  133. <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.summary.views.toLocaleString()}</p>
  134. </div>
  135. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
  136. <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.clicks')}</h3>
  137. <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.summary.clicks.toLocaleString()}</p>
  138. </div>
  139. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
  140. <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.ctr')}</h3>
  141. <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.ctr}</p>
  142. </div>
  143. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
  144. <h3 className="text-gray-500 dark:text-gray-400 text-sm font-medium">{t('analytics.page.unique_visitors')}</h3>
  145. <p className="text-3xl font-bold text-gray-900 dark:text-white mt-1">{processedAnalytics.summary.uniqueVisitors.toLocaleString()}</p>
  146. </div>
  147. </div>
  148. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
  149. <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.performance')}</h3>
  150. <ResponsiveContainer width="100%" height={350}>
  151. <LineChart data={processedAnalytics.chartData}>
  152. <CartesianGrid strokeDasharray="3 3" stroke={chartColors.grid} />
  153. <XAxis dataKey="date" stroke={chartColors.text} fontSize={12} />
  154. <YAxis stroke={chartColors.text} fontSize={12} />
  155. <Tooltip contentStyle={{ backgroundColor: chartColors.tooltipBg, border: `1px solid ${chartColors.tooltipBorder}` }} />
  156. <Legend wrapperStyle={{ color: chartColors.text } as any} />
  157. <Line type="monotone" dataKey="views" name={t('analytics.page.views')} stroke="#10b981" strokeWidth={2} activeDot={{ r: 8 }} dot={false} />
  158. <Line type="monotone" dataKey="clicks" name={t('analytics.page.clicks')} stroke="#3b82f6" strokeWidth={2} activeDot={{ r: 8 }} dot={false} />
  159. </LineChart>
  160. </ResponsiveContainer>
  161. </div>
  162. <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
  163. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg lg:col-span-1">
  164. <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.device_breakdown')}</h3>
  165. <ResponsiveContainer width="100%" height={300}>
  166. <PieChart>
  167. <Pie
  168. // @ts-ignore The recharts Pie component's type definition is missing the 'activeIndex' prop.
  169. activeIndex={activeIndex}
  170. activeShape={renderActiveShape}
  171. data={processedAnalytics.deviceData}
  172. cx="50%"
  173. cy="50%"
  174. innerRadius={60}
  175. outerRadius={80}
  176. fill="#8884d8"
  177. dataKey="value"
  178. onMouseEnter={onPieEnter}
  179. >
  180. {processedAnalytics.deviceData.map((entry, index) => (
  181. <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
  182. ))}
  183. </Pie>
  184. </PieChart>
  185. </ResponsiveContainer>
  186. </div>
  187. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg lg:col-span-2">
  188. <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.top_locations')}</h3>
  189. <ResponsiveContainer width="100%" height={300}>
  190. <BarChart data={processedAnalytics.topLocations} layout="vertical" margin={{ top: 5, right: 20, left: 20, bottom: 5 }}>
  191. <CartesianGrid strokeDasharray="3 3" stroke={chartColors.grid} horizontal={false} />
  192. <XAxis type="number" stroke={chartColors.text} fontSize={12} />
  193. <YAxis type="category" dataKey="country" stroke={chartColors.text} fontSize={12} width={80} />
  194. <Tooltip contentStyle={{ backgroundColor: chartColors.tooltipBg, border: `1px solid ${chartColors.tooltipBorder}` }} cursor={{fill: isDarkMode ? '#374151' : '#f3f4f6'}}/>
  195. <Bar dataKey="visits" fill="#10b981" />
  196. </BarChart>
  197. </ResponsiveContainer>
  198. </div>
  199. </div>
  200. <div className="bg-white dark:bg-gray-800 p-6 rounded-lg">
  201. <h3 className="font-semibold text-gray-900 dark:text-white mb-4">{t('analytics.page.top_referrers')}</h3>
  202. <ResponsiveContainer width="100%" height={300}>
  203. <BarChart data={processedAnalytics.topReferrers} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
  204. <CartesianGrid strokeDasharray="3 3" stroke={chartColors.grid} vertical={false} />
  205. <XAxis dataKey="source" stroke={chartColors.text} fontSize={12}/>
  206. <YAxis stroke={chartColors.text} fontSize={12}/>
  207. <Tooltip contentStyle={{ backgroundColor: chartColors.tooltipBg, border: `1px solid ${chartColors.tooltipBorder}` }} cursor={{fill: isDarkMode ? '#374151' : '#f3f4f6'}}/>
  208. <Bar dataKey="visits" fill="#10b981" />
  209. </BarChart>
  210. </ResponsiveContainer>
  211. </div>
  212. </div>
  213. </div>
  214. );
  215. };
  216. export default PageAnalytics;