Add gorgias component and services
This commit is contained in:
365
examples DO NOT USE OR EDIT/EXAMPLE ONLY AnalyticsDashboard.jsx
Normal file
365
examples DO NOT USE OR EDIT/EXAMPLE ONLY AnalyticsDashboard.jsx
Normal file
@@ -0,0 +1,365 @@
|
||||
//src/components/dashboard/AnalyticsDashboard.jsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { googleAnalyticsService } from "../../services/googleAnalyticsService";
|
||||
|
||||
export const AnalyticsDashboard = () => {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [timeRange, setTimeRange] = useState("30");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await googleAnalyticsService.getBasicMetrics({
|
||||
startDate: `${timeRange}daysAgo`,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const processedData = result.map((item) => ({
|
||||
...item,
|
||||
date: formatGADate(item.date),
|
||||
}));
|
||||
|
||||
const sortedData = processedData.sort((a, b) => a.date - b.date);
|
||||
|
||||
setData(sortedData);
|
||||
} else {
|
||||
console.log("No result data received");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch analytics:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [timeRange]);
|
||||
const formatGADate = (gaDate) => {
|
||||
const year = gaDate.substring(0, 4);
|
||||
const month = gaDate.substring(4, 6);
|
||||
const day = gaDate.substring(6, 8);
|
||||
return new Date(year, month - 1, day);
|
||||
};
|
||||
|
||||
const [selectedMetrics, setSelectedMetrics] = useState({
|
||||
activeUsers: true,
|
||||
newUsers: true,
|
||||
pageViews: true,
|
||||
conversions: true,
|
||||
});
|
||||
|
||||
const MetricToggle = ({ label, checked, onChange }) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={label}
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
className="dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
htmlFor={label}
|
||||
className="text-sm font-medium leading-none text-gray-900 dark:text-gray-200 peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CustomLegend = ({ metrics, selectedMetrics }) => {
|
||||
// Separate items for left and right axes
|
||||
const leftAxisItems = Object.entries(metrics).filter(
|
||||
([key, metric]) => metric.yAxis === "left" && selectedMetrics[key]
|
||||
);
|
||||
|
||||
const rightAxisItems = Object.entries(metrics).filter(
|
||||
([key, metric]) => metric.yAxis === "right" && selectedMetrics[key]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between mt-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h4 className="font-semibold text-gray-700 dark:text-gray-300">
|
||||
Left Axis
|
||||
</h4>
|
||||
{leftAxisItems.map(([key, metric]) => (
|
||||
<div key={key} className="flex items-center space-x-2">
|
||||
<div
|
||||
style={{ backgroundColor: metric.color }}
|
||||
className="w-3 h-3 rounded-full"
|
||||
></div>
|
||||
<span className="text-gray-900 dark:text-gray-100">
|
||||
{metric.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h4 className="font-semibold text-gray-700 dark:text-gray-300">
|
||||
Right Axis
|
||||
</h4>
|
||||
{rightAxisItems.map(([key, metric]) => (
|
||||
<div key={key} className="flex items-center space-x-2">
|
||||
<div
|
||||
style={{ backgroundColor: metric.color }}
|
||||
className="w-3 h-3 rounded-full"
|
||||
></div>
|
||||
<span className="text-gray-900 dark:text-gray-100">
|
||||
{metric.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const metrics = {
|
||||
activeUsers: { label: "Active Users", color: "#8b5cf6" },
|
||||
newUsers: { label: "New Users", color: "#10b981" },
|
||||
pageViews: { label: "Page Views", color: "#f59e0b" },
|
||||
conversions: { label: "Conversions", color: "#3b82f6" },
|
||||
};
|
||||
|
||||
const calculateSummary = () => {
|
||||
if (!data.length) return null;
|
||||
|
||||
const totals = data.reduce(
|
||||
(acc, day) => ({
|
||||
activeUsers: acc.activeUsers + (Number(day.activeUsers) || 0),
|
||||
newUsers: acc.newUsers + (Number(day.newUsers) || 0),
|
||||
pageViews: acc.pageViews + (Number(day.pageViews) || 0),
|
||||
conversions: acc.conversions + (Number(day.conversions) || 0),
|
||||
avgSessionDuration:
|
||||
acc.avgSessionDuration + (Number(day.avgSessionDuration) || 0),
|
||||
bounceRate: acc.bounceRate + (Number(day.bounceRate) || 0),
|
||||
}),
|
||||
{
|
||||
activeUsers: 0,
|
||||
newUsers: 0,
|
||||
pageViews: 0,
|
||||
conversions: 0,
|
||||
avgSessionDuration: 0,
|
||||
bounceRate: 0,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...totals,
|
||||
avgSessionDuration: totals.avgSessionDuration / data.length,
|
||||
bounceRate: totals.bounceRate / data.length,
|
||||
};
|
||||
};
|
||||
|
||||
const summary = calculateSummary();
|
||||
|
||||
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 formatXAxisDate = (date) => {
|
||||
if (!(date instanceof Date)) return "";
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
};
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }) => {
|
||||
if (active && payload && payload.length) {
|
||||
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 mb-2 text-gray-900 dark:text-gray-100">
|
||||
{label instanceof Date ? label.toLocaleDateString() : label}
|
||||
</p>
|
||||
{payload.map((entry, index) => (
|
||||
<p key={index} className="text-sm" style={{ color: entry.color }}>
|
||||
{`${entry.name}: ${Number(entry.value).toLocaleString()}`}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
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">
|
||||
Analytics Overview
|
||||
</CardTitle>
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger className="w-36 bg-white dark:bg-gray-800">
|
||||
<SelectValue placeholder="Select range" />
|
||||
</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>
|
||||
|
||||
{summary && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Total Users
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{summary.activeUsers.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
New Users
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{summary.newUsers.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Page Views
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{summary.pageViews.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Conversions
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{summary.conversions.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-4 mt-4">
|
||||
<MetricToggle
|
||||
label="Active Users"
|
||||
checked={selectedMetrics.activeUsers}
|
||||
onChange={(checked) =>
|
||||
setSelectedMetrics((prev) => ({ ...prev, activeUsers: checked }))
|
||||
}
|
||||
/>
|
||||
<MetricToggle
|
||||
label="New Users"
|
||||
checked={selectedMetrics.newUsers}
|
||||
onChange={(checked) =>
|
||||
setSelectedMetrics((prev) => ({ ...prev, newUsers: checked }))
|
||||
}
|
||||
/>
|
||||
<MetricToggle
|
||||
label="Page Views"
|
||||
checked={selectedMetrics.pageViews}
|
||||
onChange={(checked) =>
|
||||
setSelectedMetrics((prev) => ({ ...prev, pageViews: checked }))
|
||||
}
|
||||
/>
|
||||
<MetricToggle
|
||||
label="Conversions"
|
||||
checked={selectedMetrics.conversions}
|
||||
onChange={(checked) =>
|
||||
setSelectedMetrics((prev) => ({ ...prev, conversions: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="h-96">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{ top: 5, right: 5, left: -10, bottom: 5 }}
|
||||
animationBegin={0}
|
||||
animationDuration={1000}
|
||||
>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="#e5e7eb"
|
||||
className="dark:stroke-gray-800"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatXAxisDate}
|
||||
type="category"
|
||||
tick={{ fill: "#6b7280" }}
|
||||
stroke="#9ca3af"
|
||||
className="dark:text-gray-400"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
tick={{ fill: "#6b7280" }}
|
||||
stroke="#9ca3af"
|
||||
className="dark:text-gray-400"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tick={{ fill: "#6b7280" }}
|
||||
stroke="#9ca3af"
|
||||
className="dark:text-gray-400"
|
||||
domain={[0, "auto"]}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
formatter={(value) => (
|
||||
<span className="text-gray-900 dark:text-gray-100">
|
||||
{value}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
{/* Always render Lines and control visibility */}
|
||||
{Object.entries(metrics).map(([key, { color, label }]) => (
|
||||
<Line
|
||||
key={key}
|
||||
yAxisId={key === "pageViews" ? "right" : "left"}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={color}
|
||||
name={label}
|
||||
strokeWidth={2}
|
||||
dot={{ strokeWidth: 2, r: 2, fill: color }}
|
||||
activeDot={{ r: 6, fill: color }}
|
||||
hide={!selectedMetrics[key]} // Control visibility
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default AnalyticsDashboard;
|
||||
Reference in New Issue
Block a user