Add gorgias component and services
This commit is contained in:
529
examples DO NOT USE OR EDIT/EXAMPLE ONLY RealtimeAnalytics.jsx
Normal file
529
examples DO NOT USE OR EDIT/EXAMPLE ONLY RealtimeAnalytics.jsx
Normal file
@@ -0,0 +1,529 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from "recharts";
|
||||
import { Loader2, AlertTriangle } from "lucide-react";
|
||||
import {
|
||||
Tooltip as UITooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipProvider,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import { googleAnalyticsService } from "../../services/googleAnalyticsService";
|
||||
import { format } from "date-fns";
|
||||
|
||||
const formatNumber = (value, decimalPlaces = 0) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(value || 0);
|
||||
};
|
||||
|
||||
const formatPercent = (value, decimalPlaces = 1) =>
|
||||
`${(value || 0).toFixed(decimalPlaces)}%`;
|
||||
|
||||
const summaryCard = (label, sublabel, value, options = {}) => {
|
||||
const {
|
||||
isMonetary = false,
|
||||
isPercentage = false,
|
||||
decimalPlaces = 0,
|
||||
} = options;
|
||||
|
||||
let displayValue;
|
||||
if (isMonetary) {
|
||||
displayValue = formatCurrency(value, decimalPlaces);
|
||||
} else if (isPercentage) {
|
||||
displayValue = formatPercent(value, decimalPlaces);
|
||||
} else {
|
||||
displayValue = formatNumber(value, decimalPlaces);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-lg md:text-sm lg:text-lg text-gray-900 dark:text-gray-100">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{displayValue}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">{sublabel}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const QuotaInfo = ({ tokenQuota }) => {
|
||||
// Add early return if tokenQuota is null or undefined
|
||||
if (!tokenQuota || typeof tokenQuota !== "object") return null;
|
||||
|
||||
const {
|
||||
projectHourly = {},
|
||||
daily = {},
|
||||
serverErrors = {},
|
||||
thresholdedRequests = {},
|
||||
} = tokenQuota;
|
||||
|
||||
// Add null checks and default values for all properties
|
||||
const {
|
||||
remaining: projectHourlyRemaining = 0,
|
||||
consumed: projectHourlyConsumed = 0,
|
||||
} = projectHourly;
|
||||
|
||||
const { remaining: dailyRemaining = 0, consumed: dailyConsumed = 0 } = daily;
|
||||
|
||||
const { remaining: errorsRemaining = 10, consumed: errorsConsumed = 0 } =
|
||||
serverErrors;
|
||||
|
||||
const {
|
||||
remaining: thresholdRemaining = 120,
|
||||
consumed: thresholdConsumed = 0,
|
||||
} = thresholdedRequests;
|
||||
|
||||
// Calculate percentages with safe math
|
||||
const hourlyPercentage = ((projectHourlyRemaining / 14000) * 100).toFixed(1);
|
||||
const dailyPercentage = ((dailyRemaining / 200000) * 100).toFixed(1);
|
||||
const errorPercentage = ((errorsRemaining / 10) * 100).toFixed(1);
|
||||
const thresholdPercentage = ((thresholdRemaining / 120) * 100).toFixed(1);
|
||||
|
||||
// Determine color based on remaining percentage
|
||||
const getStatusColor = (percentage) => {
|
||||
const numericPercentage = parseFloat(percentage);
|
||||
if (isNaN(numericPercentage) || numericPercentage < 20)
|
||||
return "text-red-500 dark:text-red-400";
|
||||
if (numericPercentage < 40) return "text-yellow-500 dark:text-yellow-400";
|
||||
return "text-green-500 dark:text-green-400";
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<UITooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 space-x-1">
|
||||
<span>Quota:</span>
|
||||
<span className={`font-medium ${getStatusColor(hourlyPercentage)}`}>
|
||||
{hourlyPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-white dark:bg-gray-800 border dark:border-gray-700">
|
||||
<div className="space-y-3 p-2">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
Project Hourly
|
||||
</div>
|
||||
<div className={`${getStatusColor(hourlyPercentage)}`}>
|
||||
{projectHourlyRemaining.toLocaleString()} / 14,000 remaining
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
Daily
|
||||
</div>
|
||||
<div className={`${getStatusColor(dailyPercentage)}`}>
|
||||
{dailyRemaining.toLocaleString()} / 200,000 remaining
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
Server Errors
|
||||
</div>
|
||||
<div className={`${getStatusColor(errorPercentage)}`}>
|
||||
{errorsConsumed} / 10 used this hour
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
Thresholded Requests
|
||||
</div>
|
||||
<div className={`${getStatusColor(thresholdPercentage)}`}>
|
||||
{thresholdConsumed} / 120 used this hour
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</UITooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
const RealtimeAnalytics = () => {
|
||||
const [basicData, setBasicData] = useState({
|
||||
last30MinUsers: 0,
|
||||
last5MinUsers: 0,
|
||||
byMinute: [],
|
||||
tokenQuota: null,
|
||||
lastUpdated: null,
|
||||
});
|
||||
|
||||
const [detailedData, setDetailedData] = useState({
|
||||
currentPages: [],
|
||||
sources: [],
|
||||
recentEvents: [],
|
||||
lastUpdated: null,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let basicInterval;
|
||||
let detailedInterval;
|
||||
|
||||
const fetchBasicData = async () => {
|
||||
if (isPaused) return;
|
||||
try {
|
||||
const response = await fetch("/api/analytics/realtime/basic", {
|
||||
credentials: "include",
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
const processed = await googleAnalyticsService.getRealTimeBasicData();
|
||||
setBasicData(processed);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error("Error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
response: error.response,
|
||||
});
|
||||
if (error.message === "QUOTA_EXCEEDED") {
|
||||
setError("Quota exceeded. Analytics paused until manually resumed.");
|
||||
setIsPaused(true);
|
||||
} else {
|
||||
setError("Failed to fetch analytics data");
|
||||
}
|
||||
}
|
||||
};
|
||||
const fetchDetailedData = async () => {
|
||||
if (isPaused) return;
|
||||
try {
|
||||
const result = await googleAnalyticsService.getRealTimeDetailedData();
|
||||
setDetailedData(result);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch detailed realtime data:", error);
|
||||
if (error.message === "QUOTA_EXCEEDED") {
|
||||
setError("Quota exceeded. Analytics paused until manually resumed.");
|
||||
setIsPaused(true);
|
||||
} else {
|
||||
setError("Failed to fetch analytics data");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial fetches
|
||||
fetchBasicData();
|
||||
fetchDetailedData();
|
||||
|
||||
// Set up intervals
|
||||
basicInterval = setInterval(fetchBasicData, 30000); // 30 seconds
|
||||
detailedInterval = setInterval(fetchDetailedData, 300000); // 5 minutes
|
||||
|
||||
return () => {
|
||||
clearInterval(basicInterval);
|
||||
clearInterval(detailedInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const togglePause = () => {
|
||||
setIsPaused(!isPaused);
|
||||
};
|
||||
|
||||
if (loading && !basicData && !detailedData) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Pie chart colors
|
||||
const COLORS = [
|
||||
"#8b5cf6",
|
||||
"#10b981",
|
||||
"#f59e0b",
|
||||
"#3b82f6",
|
||||
"#0088FE",
|
||||
"#00C49F",
|
||||
"#FFBB28",
|
||||
];
|
||||
|
||||
// Handle 'other' in data
|
||||
const totalUsers = detailedData.sources.reduce(
|
||||
(sum, source) => sum + source.users,
|
||||
0
|
||||
);
|
||||
const sourcesData = detailedData.sources.map((source) => {
|
||||
const percent = (source.users / totalUsers) * 100;
|
||||
return { ...source, percent };
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="bg-white dark:bg-gray-900">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
Realtime Analytics
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-4">
|
||||
{basicData?.data?.quotaInfo && (
|
||||
<QuotaInfo tokenQuota={basicData.data.quotaInfo} />
|
||||
)}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Last updated:{" "}
|
||||
{basicData.lastUpdated
|
||||
? format(new Date(basicData.lastUpdated), "p")
|
||||
: "N/A"}
|
||||
</div>
|
||||
</div>{" "}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mt-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||
{[
|
||||
{
|
||||
label: "Last 30 Minutes",
|
||||
sublabel: "Active Users",
|
||||
value: basicData.last30MinUsers,
|
||||
},
|
||||
{
|
||||
label: "Last 5 Minutes",
|
||||
sublabel: "Active Users",
|
||||
value: basicData.last5MinUsers,
|
||||
},
|
||||
].map((card) => (
|
||||
<div key={card.label}>
|
||||
{summaryCard(card.label, card.sublabel, card.value)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4">
|
||||
{/* User Activity Chart */}
|
||||
<div className="mb-6">
|
||||
<Card className="bg-white dark:bg-gray-900">
|
||||
<CardHeader className="pb-1">
|
||||
<CardTitle className="text-md font-bold">
|
||||
Active Users Per Minute
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={basicData.byMinute}
|
||||
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||
>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 border dark:border-gray-700 p-2 rounded shadow">
|
||||
<p className="text-gray-900 dark:text-gray-100">{`${payload[0].value} active users`}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{payload[0].payload.timestamp}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>{" "}
|
||||
<Bar dataKey="users" fill="#3b82f6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs for Detailed Data */}
|
||||
<Tabs defaultValue="pages" className="w-full">
|
||||
<TabsList className="p-0 sm:p-1 md:p-0 lg:p-1 -ml-4 sm:ml-0 md:-ml-4 lg:ml-0">
|
||||
<TabsTrigger value="pages">Current Pages</TabsTrigger>
|
||||
<TabsTrigger value="events">Recent Events</TabsTrigger>
|
||||
<TabsTrigger value="sources">Active Devices</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Current Pages Tab */}
|
||||
<TabsContent
|
||||
value="pages"
|
||||
className="mt-4 space-y-2 h-full max-h-[400px] md:max-h[400px] lg:max-h-[540px] xl: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 text-xs dark:hover:scrollbar-thumb-gray-600 pr-2"
|
||||
>
|
||||
<div className="text-xs text-right text-gray-500 dark:text-gray-400">
|
||||
Last updated:{" "}
|
||||
{detailedData.lastUpdated
|
||||
? format(new Date(detailedData.lastUpdated), "p")
|
||||
: "N/A"}
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="dark:border-gray-800">
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">
|
||||
Page
|
||||
</TableHead>
|
||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
||||
Views
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{detailedData.currentPages.map(({ page, views }, index) => (
|
||||
<TableRow key={index} className="dark:border-gray-800">
|
||||
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{page}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
||||
{formatNumber(views)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
{/* Recent Events Tab */}
|
||||
<TabsContent
|
||||
value="events"
|
||||
className="mt-4 space-y-2 h-full max-h-[400px] md:max-h[400px] lg:max-h-[540px] xl: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 text-xs dark:hover:scrollbar-thumb-gray-600 pr-2"
|
||||
>
|
||||
<div className="text-xs text-right text-gray-500 dark:text-gray-400">
|
||||
Last updated:{" "}
|
||||
{detailedData.lastUpdated
|
||||
? format(new Date(detailedData.lastUpdated), "p")
|
||||
: "N/A"}
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="dark:border-gray-800">
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">
|
||||
Event
|
||||
</TableHead>
|
||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
||||
Count
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{detailedData.recentEvents.map(({ event, count }, index) => (
|
||||
<TableRow key={index} className="dark:border-gray-800">
|
||||
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{event}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
||||
{formatNumber(count)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
{/* Active Devices Tab */}
|
||||
<TabsContent
|
||||
value="sources"
|
||||
className="mt-4 space-y-2 h-full max-h-[400px] md:max-h[400px] lg:max-h-[540px] xl: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 text-xs dark:hover:scrollbar-thumb-gray-600 pr-2"
|
||||
>
|
||||
{" "}
|
||||
<div className="text-xs text-right text-gray-500 dark:text-gray-400">
|
||||
Last updated:{" "}
|
||||
{detailedData.lastUpdated
|
||||
? format(new Date(detailedData.lastUpdated), "p")
|
||||
: "N/A"}
|
||||
</div>
|
||||
<div className="h-60">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={sourcesData}
|
||||
dataKey="users"
|
||||
nameKey="device"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
labelLine={false}
|
||||
label={({ device, percent }) =>
|
||||
`${device}: ${percent.toFixed(0)}%`
|
||||
}
|
||||
>
|
||||
{sourcesData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 border dark:border-gray-700 p-2 rounded shadow">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{payload[0].payload.device}
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">{`${formatNumber(
|
||||
payload[0].value
|
||||
)} users`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</PieChart>{" "}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||
{sourcesData.map((source, index) => (
|
||||
<div
|
||||
key={index}
|
||||
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">
|
||||
{source.device}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(source.users)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{source.percent.toFixed(0)}% of users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default RealtimeAnalytics;
|
||||
Reference in New Issue
Block a user