298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
Legend,
|
|
} from "recharts";
|
|
import { Loader2 } from "lucide-react";
|
|
import { googleAnalyticsService } from "../../services/googleAnalyticsService";
|
|
|
|
export const UserBehaviorDashboard = () => {
|
|
const [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [timeRange, setTimeRange] = useState("30");
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const result = await googleAnalyticsService.getUserBehavior(timeRange);
|
|
if (result) {
|
|
setData(result);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch behavior data:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [timeRange]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card className="bg-white dark:bg-gray-900">
|
|
<CardContent className="h-96 flex items-center justify-center">
|
|
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const COLORS = {
|
|
desktop: "#8b5cf6", // Purple
|
|
mobile: "#10b981", // Green
|
|
tablet: "#f59e0b", // Yellow
|
|
};
|
|
|
|
const deviceData = data?.data?.pageData?.deviceData || [];
|
|
const totalViews = deviceData.reduce((sum, item) => sum + item.pageViews, 0);
|
|
const totalSessions = deviceData.reduce(
|
|
(sum, item) => sum + item.sessions,
|
|
0
|
|
);
|
|
|
|
const CustomTooltip = ({ active, payload }) => {
|
|
if (active && payload && payload.length) {
|
|
const data = payload[0].payload;
|
|
const percentage = ((data.pageViews / totalViews) * 100).toFixed(1);
|
|
const sessionPercentage = ((data.sessions / totalSessions) * 100).toFixed(
|
|
1
|
|
);
|
|
return (
|
|
<div className="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-lg shadow-lg p-3">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{data.device}
|
|
</p>
|
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
{data.pageViews.toLocaleString()} views ({percentage}%)
|
|
</p>
|
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
{data.sessions.toLocaleString()} sessions ({sessionPercentage}%)
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const formatDuration = (seconds) => {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = Math.floor(seconds % 60);
|
|
return `${minutes}m ${remainingSeconds}s`;
|
|
};
|
|
|
|
return (
|
|
<Card className="bg-white dark:bg-gray-900">
|
|
<CardHeader>
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
User Behavior Analysis
|
|
</CardTitle>
|
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
|
<SelectTrigger className="w-36 bg-white dark:bg-gray-800">
|
|
<SelectValue>
|
|
{timeRange === "7" && "Last 7 days"}
|
|
{timeRange === "14" && "Last 14 days"}
|
|
{timeRange === "30" && "Last 30 days"}
|
|
{timeRange === "90" && "Last 90 days"}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="7">Last 7 days</SelectItem>
|
|
<SelectItem value="14">Last 14 days</SelectItem>
|
|
<SelectItem value="30">Last 30 days</SelectItem>
|
|
<SelectItem value="90">Last 90 days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs defaultValue="pages" className="w-full">
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger value="pages">Top Pages</TabsTrigger>
|
|
<TabsTrigger value="sources">Traffic Sources</TabsTrigger>
|
|
<TabsTrigger value="devices">Device Usage</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent
|
|
value="pages"
|
|
className="mt-4 space-y-2 h-full max-h-[440px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2"
|
|
>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="dark:border-gray-800">
|
|
<TableHead className="text-gray-900 dark:text-gray-100">
|
|
Page Path
|
|
</TableHead>
|
|
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
Views
|
|
</TableHead>
|
|
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
Bounce Rate
|
|
</TableHead>
|
|
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
Avg. Duration
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data?.data?.pageData?.pageData.map((page, index) => (
|
|
<TableRow key={index} className="dark:border-gray-800">
|
|
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
|
{page.path}
|
|
</TableCell>
|
|
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
|
{page.pageViews.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
|
{page.bounceRate.toFixed(1)}%
|
|
</TableCell>
|
|
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
|
{formatDuration(page.avgSessionDuration)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TabsContent>
|
|
|
|
<TabsContent
|
|
value="sources"
|
|
className="mt-4 space-y-2 h-full max-h-[440px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2"
|
|
>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="dark:border-gray-800">
|
|
<TableHead className="text-gray-900 dark:text-gray-100">
|
|
Source
|
|
</TableHead>
|
|
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
Sessions
|
|
</TableHead>
|
|
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
Conversions
|
|
</TableHead>
|
|
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
Conv. Rate
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data?.data?.sourceData?.map((source, index) => (
|
|
<TableRow key={index} className="dark:border-gray-800">
|
|
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
|
{source.source}
|
|
</TableCell>
|
|
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
|
{source.sessions.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
|
{source.conversions.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
|
{((source.conversions / source.sessions) * 100).toFixed(
|
|
1
|
|
)}
|
|
%
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TabsContent>
|
|
|
|
<TabsContent
|
|
value="devices"
|
|
className="mt-4 space-y-2 h-full max-h-[440px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2"
|
|
>
|
|
<div className="h-60">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={deviceData}
|
|
dataKey="pageViews"
|
|
nameKey="device"
|
|
cx="50%"
|
|
cy="50%"
|
|
outerRadius={80}
|
|
labelLine={false}
|
|
label={({ name, percent }) =>
|
|
`${name} ${(percent * 100).toFixed(1)}%`
|
|
}
|
|
>
|
|
{deviceData.map((entry, index) => (
|
|
<Cell
|
|
key={`cell-${index}`}
|
|
fill={COLORS[entry.device.toLowerCase()]}
|
|
/>
|
|
))}
|
|
</Pie>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Legend
|
|
formatter={(value) => (
|
|
<span className="text-gray-900 dark:text-gray-100">
|
|
{value}
|
|
</span>
|
|
)}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{deviceData.map((device) => (
|
|
<div
|
|
key={device.device}
|
|
className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800"
|
|
>
|
|
<div className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
{device.device}
|
|
</div>
|
|
<div className="mt-0">
|
|
<div className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
|
{device.pageViews.toLocaleString()}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
{((device.pageViews / totalViews) * 100).toFixed(1)}% of
|
|
views
|
|
</div>
|
|
</div>
|
|
<div className="mt-1">
|
|
<div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
{device.sessions.toLocaleString()}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
{((device.sessions / totalSessions) * 100).toFixed(1)}% of
|
|
sessions
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|