Adjust app/component layouts, restyle analyticsdashboard and realtimeanalytics

This commit is contained in:
2024-12-28 00:46:06 -05:00
parent dc9a2f350a
commit 7291221154
5 changed files with 512 additions and 393 deletions

View File

@@ -93,9 +93,6 @@ const DashboardLayout = () => {
<Header /> <Header />
</div> </div>
<Navigation /> <Navigation />
<AnalyticsDashboard />
<UserBehaviorDashboard />
<RealtimeAnalytics />
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
<div className="xl:col-span-4 col-span-6"> <div className="xl:col-span-4 col-span-6">
@@ -105,26 +102,36 @@ const DashboardLayout = () => {
</div> </div>
</div> </div>
</div> </div>
<div id="feed-xl" className="xl:col-span-2 col-span-6 h-[500px] xl:h-[643px] 2xl:h-[510px] lg:hidden xl:block"> <div id="realtime" className="xl:col-span-2 col-span-6 h-[508px] overflow-auto">
<div className="h-full"> <div className="h-full">
<div className="h-full"><EventFeed /></div> <RealtimeAnalytics />
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<div id="feed-lg" className="hidden lg:col-span-6 lg:block xl:hidden h-[740px]"> <div id="feed" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
<EventFeed /> <EventFeed />
</div> </div>
<div id="products" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
<ProductGrid />
</div>
<div id="sales" className="col-span-12 xl:col-span-8 h-full w-full flex"> <div id="sales" className="col-span-12 xl:col-span-8 h-full w-full flex">
<SalesChart className="w-full h-full"/> <SalesChart className="w-full h-full"/>
</div> </div>
</div> </div>
<div id="campaigns"> <div className="grid grid-cols-12 gap-4">
<div id="product-grid" className="col-span-4 h-[500px]">
<ProductGrid />
</div>
<div id="campaigns" className="col-span-8">
<KlaviyoCampaigns /> <KlaviyoCampaigns />
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-8">
<AnalyticsDashboard />
</div>
<div className="col-span-4">
<UserBehaviorDashboard />
</div>
</div>
<div id="meta-campaigns"> <div id="meta-campaigns">
<MetaCampaigns /> <MetaCampaigns />
</div> </div>

View File

@@ -7,7 +7,8 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { import {
LineChart, LineChart,
Line, Line,
@@ -17,13 +18,130 @@ import {
Tooltip, Tooltip,
Legend, Legend,
ResponsiveContainer, ResponsiveContainer,
ReferenceLine,
} from "recharts"; } from "recharts";
import { Loader2 } from "lucide-react"; import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Skeleton } from "@/components/ui/skeleton";
// Add helper function for currency formatting
const formatCurrency = (value, useFractionDigits = true) => {
if (typeof value !== "number") return "$0.00";
const roundedValue = parseFloat(value.toFixed(useFractionDigits ? 2 : 0));
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: useFractionDigits ? 2 : 0,
maximumFractionDigits: useFractionDigits ? 2 : 0,
}).format(roundedValue);
};
// Add skeleton components
const SkeletonChart = () => (
<div className="h-[400px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="h-full flex flex-col">
<div className="flex-1 relative">
<div className="h-full w-full relative">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
style={{ top: `${20 + i * 20}%` }}
/>
))}
<div
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
style={{
opacity: 0.2,
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
}}
/>
</div>
</div>
</div>
</div>
);
const SkeletonStats = () => (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32 mb-2" />
<Skeleton className="h-4 w-24" />
</CardContent>
</Card>
))}
</div>
);
// Add StatCard component
const StatCard = ({
title,
value,
description,
trend,
trendValue,
colorClass = "text-gray-900 dark:text-gray-100",
}) => (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
<span className="text-sm text-muted-foreground">{title}</span>
{trend && (
<span
className={`text-sm flex items-center gap-1 ${
trend === "up"
? "text-emerald-600 dark:text-emerald-400"
: "text-rose-600 dark:text-rose-400"
}`}
>
{trendValue}
</span>
)}
</CardHeader>
<CardContent className="p-4 pt-0">
<div className={`text-2xl font-bold mb-1 ${colorClass}`}>{value}</div>
{description && (
<div className="text-sm text-muted-foreground">{description}</div>
)}
</CardContent>
</Card>
);
// Add color constants
const METRIC_COLORS = {
activeUsers: {
color: "#8b5cf6",
className: "text-purple-600 dark:text-purple-400",
},
newUsers: {
color: "#10b981",
className: "text-emerald-600 dark:text-emerald-400",
},
pageViews: {
color: "#f59e0b",
className: "text-amber-600 dark:text-amber-400",
},
conversions: {
color: "#3b82f6",
className: "text-blue-600 dark:text-blue-400",
},
};
export const AnalyticsDashboard = () => { export const AnalyticsDashboard = () => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState("30"); const [timeRange, setTimeRange] = useState("30");
const [metrics, setMetrics] = useState({
activeUsers: true,
newUsers: true,
pageViews: true,
conversions: true,
});
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -75,175 +193,79 @@ export const AnalyticsDashboard = () => {
return new Date(year, month - 1, day); return new Date(year, month - 1, day);
}; };
const [selectedMetrics, setSelectedMetrics] = useState({ const formatXAxis = (date) => {
activeUsers: true, if (!date) return "";
newUsers: true, return date.toLocaleDateString([], { month: "short", day: "numeric" });
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 = { const calculateSummaryStats = () => {
activeUsers: {
label: "Active Users",
color: "#8b5cf6",
yAxis: "left"
},
newUsers: {
label: "New Users",
color: "#10b981",
yAxis: "left"
},
pageViews: {
label: "Page Views",
color: "#f59e0b",
yAxis: "right"
},
conversions: {
label: "Conversions",
color: "#3b82f6",
yAxis: "right"
},
};
const calculateSummary = () => {
if (!data.length) return null; if (!data.length) return null;
const totals = data.reduce( const totals = data.reduce(
(acc, day) => ({ (acc, day) => ({
activeUsers: acc.activeUsers + (Number(day.activeUsers) || 0), activeUsers: acc.activeUsers + day.activeUsers,
newUsers: acc.newUsers + (Number(day.newUsers) || 0), newUsers: acc.newUsers + day.newUsers,
pageViews: acc.pageViews + (Number(day.pageViews) || 0), pageViews: acc.pageViews + day.pageViews,
conversions: acc.conversions + (Number(day.conversions) || 0), conversions: acc.conversions + day.conversions,
avgSessionDuration:
acc.avgSessionDuration + (Number(day.avgSessionDuration) || 0),
bounceRate: acc.bounceRate + (Number(day.bounceRate) || 0),
}), }),
{ {
activeUsers: 0, activeUsers: 0,
newUsers: 0, newUsers: 0,
pageViews: 0, pageViews: 0,
conversions: 0, conversions: 0,
avgSessionDuration: 0,
bounceRate: 0,
} }
); );
return { const averages = {
...totals, activeUsers: totals.activeUsers / data.length,
avgSessionDuration: totals.avgSessionDuration / data.length, newUsers: totals.newUsers / data.length,
bounceRate: totals.bounceRate / data.length, pageViews: totals.pageViews / data.length,
}; conversions: totals.conversions / data.length,
}; };
const summary = calculateSummary(); return { totals, averages };
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 summaryStats = calculateSummaryStats();
const CustomTooltip = ({ active, payload, label }) => { const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-lg shadow-lg p-3"> <Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
<p className="text-sm font-medium mb-2 text-gray-900 dark:text-gray-100"> <CardContent className="p-0 space-y-2">
<p className="font-medium text-sm border-b pb-1 mb-2">
{label instanceof Date ? label.toLocaleDateString() : label} {label instanceof Date ? label.toLocaleDateString() : label}
</p> </p>
<div className="space-y-1">
{payload.map((entry, index) => ( {payload.map((entry, index) => (
<p key={index} className="text-sm" style={{ color: entry.color }}> <div
{`${entry.name}: ${Number(entry.value).toLocaleString()}`} key={index}
</p> className="flex justify-between items-center text-sm"
>
<span style={{ color: entry.color }}>{entry.name}:</span>
<span className="font-medium ml-4">
{entry.value.toLocaleString()}
</span>
</div>
))} ))}
</div> </div>
</CardContent>
</Card>
); );
} }
return null; return null;
}; };
return ( return (
<Card className="bg-white dark:bg-gray-900"> <Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader> <CardHeader className="p-6 pb-4">
<div className="flex flex-col space-y-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Analytics Overview Analytics Overview
</CardTitle> </CardTitle>
<Select value={timeRange} onValueChange={setTimeRange}> <Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-36 bg-white dark:bg-gray-800"> <SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="Select range" /> <SelectValue placeholder="Select range" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -255,142 +277,189 @@ export const AnalyticsDashboard = () => {
</Select> </Select>
</div> </div>
{summary && ( {loading ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4"> <SkeletonStats />
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800"> ) : summaryStats ? (
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4">
Total Users <StatCard
title="Active Users"
value={summaryStats.totals.activeUsers.toLocaleString()}
description={`Avg: ${Math.round(
summaryStats.averages.activeUsers
).toLocaleString()} per day`}
colorClass={METRIC_COLORS.activeUsers.className}
/>
<StatCard
title="New Users"
value={summaryStats.totals.newUsers.toLocaleString()}
description={`Avg: ${Math.round(
summaryStats.averages.newUsers
).toLocaleString()} per day`}
colorClass={METRIC_COLORS.newUsers.className}
/>
<StatCard
title="Page Views"
value={summaryStats.totals.pageViews.toLocaleString()}
description={`Avg: ${Math.round(
summaryStats.averages.pageViews
).toLocaleString()} per day`}
colorClass={METRIC_COLORS.pageViews.className}
/>
<StatCard
title="Conversions"
value={summaryStats.totals.conversions.toLocaleString()}
description={`Avg: ${Math.round(
summaryStats.averages.conversions
).toLocaleString()} per day`}
colorClass={METRIC_COLORS.conversions.className}
/>
</div> </div>
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100"> ) : null}
{summary.activeUsers.toLocaleString()}
</div> <div className="flex items-center flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
</div> <div className="flex flex-wrap gap-1">
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800"> <Button
<div className="text-sm text-gray-500 dark:text-gray-400"> variant={metrics.activeUsers ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
activeUsers: !prev.activeUsers,
}))
}
>
Active Users
</Button>
<Button
variant={metrics.newUsers ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
newUsers: !prev.newUsers,
}))
}
>
New Users New Users
</div> </Button>
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <Button
{summary.newUsers.toLocaleString()} variant={metrics.pageViews ? "default" : "outline"}
</div> size="sm"
</div> onClick={() =>
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800"> setMetrics((prev) => ({
<div className="text-sm text-gray-500 dark:text-gray-400"> ...prev,
pageViews: !prev.pageViews,
}))
}
>
Page Views Page Views
</div> </Button>
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <Button
{summary.pageViews.toLocaleString()} variant={metrics.conversions ? "default" : "outline"}
</div> size="sm"
</div> onClick={() =>
<div className="p-4 rounded-lg bg-gray-50 dark:bg-gray-800"> setMetrics((prev) => ({
<div className="text-sm text-gray-500 dark:text-gray-400"> ...prev,
conversions: !prev.conversions,
}))
}
>
Conversions Conversions
</div> </Button>
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{summary.conversions.toLocaleString()}
</div> </div>
</div> </div>
</div> </div>
)}
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-6 pt-0">
<div className="h-[400px] mt-4"> {loading ? (
<SkeletonChart />
) : !data.length ? (
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
<div className="text-center">
<TrendingUp className="h-12 w-12 mx-auto mb-4" />
<div className="font-medium mb-2">No analytics data available</div>
<div className="text-sm">Try selecting a different time range</div>
</div>
</div>
) : (
<div className="h-[400px] mt-4 bg-card rounded-lg p-0">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={data}> <LineChart
data={data}
margin={{ top: 5, right: 0, left: -5, bottom: 5 }}
>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
className="stroke-gray-200 dark:stroke-gray-700" className="stroke-muted"
/> />
<XAxis <XAxis
dataKey="date" dataKey="date"
tickFormatter={formatXAxisDate} tickFormatter={formatXAxis}
className="text-gray-600 dark:text-gray-300" className="text-xs"
tick={{ fill: "currentColor" }}
/> />
<YAxis <YAxis
yAxisId="left" yAxisId="left"
className="text-gray-600 dark:text-gray-300" className="text-xs"
tick={{ fill: "currentColor" }}
/> />
<YAxis <YAxis
yAxisId="right" yAxisId="right"
orientation="right" orientation="right"
className="text-gray-600 dark:text-gray-300" className="text-xs"
tick={{ fill: "currentColor" }}
/> />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Legend content={<CustomLegend metrics={metrics} selectedMetrics={selectedMetrics} />} /> <Legend />
{selectedMetrics.activeUsers && ( {metrics.activeUsers && (
<Line <Line
yAxisId="left" yAxisId="left"
type="monotone" type="monotone"
dataKey="activeUsers" dataKey="activeUsers"
name="Active Users" name="Active Users"
stroke={metrics.activeUsers.color} stroke={METRIC_COLORS.activeUsers.color}
strokeWidth={2}
dot={false} dot={false}
/> />
)} )}
{selectedMetrics.newUsers && ( {metrics.newUsers && (
<Line <Line
yAxisId="left" yAxisId="left"
type="monotone" type="monotone"
dataKey="newUsers" dataKey="newUsers"
name="New Users" name="New Users"
stroke={metrics.newUsers.color} stroke={METRIC_COLORS.newUsers.color}
strokeWidth={2}
dot={false} dot={false}
/> />
)} )}
{selectedMetrics.pageViews && ( {metrics.pageViews && (
<Line <Line
yAxisId="right" yAxisId="right"
type="monotone" type="monotone"
dataKey="pageViews" dataKey="pageViews"
name="Page Views" name="Page Views"
stroke={metrics.pageViews.color} stroke={METRIC_COLORS.pageViews.color}
strokeWidth={2}
dot={false} dot={false}
/> />
)} )}
{selectedMetrics.conversions && ( {metrics.conversions && (
<Line <Line
yAxisId="right" yAxisId="right"
type="monotone" type="monotone"
dataKey="conversions" dataKey="conversions"
name="Conversions" name="Conversions"
stroke={metrics.conversions.color} stroke={METRIC_COLORS.conversions.color}
strokeWidth={2}
dot={false} dot={false}
/> />
)} )}
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</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>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -219,7 +219,7 @@ const KlaviyoCampaigns = ({ className }) => {
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="overflow-y-auto pl-4 max-h-[350px] mb-4"> <CardContent className="overflow-y-auto pl-4 max-h-[400px] mb-4">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr> <tr>

View File

@@ -31,6 +31,21 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { format } from "date-fns"; import { format } from "date-fns";
const METRIC_COLORS = {
activeUsers: {
color: "#8b5cf6",
className: "text-purple-600 dark:text-purple-400",
},
pages: {
color: "#10b981",
className: "text-emerald-600 dark:text-emerald-400",
},
sources: {
color: "#f59e0b",
className: "text-amber-600 dark:text-amber-400",
},
};
const formatNumber = (value, decimalPlaces = 0) => { const formatNumber = (value, decimalPlaces = 0) => {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces, minimumFractionDigits: decimalPlaces,
@@ -46,6 +61,7 @@ const summaryCard = (label, sublabel, value, options = {}) => {
isMonetary = false, isMonetary = false,
isPercentage = false, isPercentage = false,
decimalPlaces = 0, decimalPlaces = 0,
colorClass = METRIC_COLORS.activeUsers.className,
} = options; } = options;
let displayValue; let displayValue;
@@ -58,15 +74,17 @@ const summaryCard = (label, sublabel, value, options = {}) => {
} }
return ( return (
<div className="p-3 rounded-lg bg-gray-50 dark:bg-gray-800"> <Card>
<div className="text-lg md:text-sm lg:text-lg text-gray-900 dark:text-gray-100"> <CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
{label} <span className="text-md">{label}</span>
</div> </CardHeader>
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <CardContent className="px-4 pt-0 pb-2">
<div className={`text-2xl font-bold mb-1 ${colorClass}`}>
{displayValue} {displayValue}
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400">{sublabel}</div> <div className="text-sm text-muted-foreground">{sublabel}</div>
</div> </CardContent>
</Card>
); );
}; };
@@ -113,20 +131,18 @@ const QuotaInfo = ({ tokenQuota }) => {
}; };
return ( return (
<TooltipProvider> <>
<UITooltip> <div className="flex items-center font-semibold rounded-md space-x-1">
<TooltipTrigger>
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 space-x-1">
<span>Quota:</span> <span>Quota:</span>
<span className={`font-medium ${getStatusColor(hourlyPercentage)}`}> <span className={`font-medium ${getStatusColor(hourlyPercentage)}`}>
{hourlyPercentage}% {hourlyPercentage}%
</span> </span>
</div> </div>
</TooltipTrigger>
<TooltipContent className="bg-white dark:bg-gray-800 border dark:border-gray-700"> <div className="dark:border-gray-700">
<div className="space-y-3 p-2"> <div className="space-y-3 mt-2">
<div> <div>
<div className="font-semibold text-gray-900 dark:text-gray-100"> <div className="font-semibold text-gray-100">
Project Hourly Project Hourly
</div> </div>
<div className={`${getStatusColor(hourlyPercentage)}`}> <div className={`${getStatusColor(hourlyPercentage)}`}>
@@ -134,7 +150,7 @@ const QuotaInfo = ({ tokenQuota }) => {
</div> </div>
</div> </div>
<div> <div>
<div className="font-semibold text-gray-900 dark:text-gray-100"> <div className="font-semibold text-gray-100">
Daily Daily
</div> </div>
<div className={`${getStatusColor(dailyPercentage)}`}> <div className={`${getStatusColor(dailyPercentage)}`}>
@@ -142,7 +158,7 @@ const QuotaInfo = ({ tokenQuota }) => {
</div> </div>
</div> </div>
<div> <div>
<div className="font-semibold text-gray-900 dark:text-gray-100"> <div className="font-semibold text-gray-100">
Server Errors Server Errors
</div> </div>
<div className={`${getStatusColor(errorPercentage)}`}> <div className={`${getStatusColor(errorPercentage)}`}>
@@ -150,7 +166,7 @@ const QuotaInfo = ({ tokenQuota }) => {
</div> </div>
</div> </div>
<div> <div>
<div className="font-semibold text-gray-900 dark:text-gray-100"> <div className="font-semibold text-gray-100">
Thresholded Requests Thresholded Requests
</div> </div>
<div className={`${getStatusColor(thresholdPercentage)}`}> <div className={`${getStatusColor(thresholdPercentage)}`}>
@@ -158,9 +174,8 @@ const QuotaInfo = ({ tokenQuota }) => {
</div> </div>
</div> </div>
</div> </div>
</TooltipContent> </div>
</UITooltip> </>
</TooltipProvider>
); );
}; };
@@ -195,7 +210,9 @@ export const RealtimeAnalytics = () => {
const matchingRow = data.timeSeriesResponse?.rows?.find( const matchingRow = data.timeSeriesResponse?.rows?.find(
(row) => parseInt(row.dimensionValues[0].value) === i (row) => parseInt(row.dimensionValues[0].value) === i
); );
const users = matchingRow ? parseInt(matchingRow.metricValues[0].value) : 0; const users = matchingRow
? parseInt(matchingRow.metricValues[0].value)
: 0;
const timestamp = new Date(Date.now() - i * 60000); const timestamp = new Date(Date.now() - i * 60000);
return { return {
minute: -i, minute: -i,
@@ -243,7 +260,9 @@ export const RealtimeAnalytics = () => {
data.eventResponse?.rows data.eventResponse?.rows
?.filter( ?.filter(
(row) => (row) =>
!["session_start", "(other)"].includes(row.dimensionValues[0].value) !["session_start", "(other)"].includes(
row.dimensionValues[0].value
)
) )
.map((row) => ({ .map((row) => ({
event: row.dimensionValues[0].value, event: row.dimensionValues[0].value,
@@ -343,31 +362,31 @@ export const RealtimeAnalytics = () => {
} }
return ( return (
<Card className="bg-white dark:bg-gray-900"> <Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
<CardHeader> <CardHeader className="p-6 pb-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-center">
<div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Real-Time Analytics Real-Time Analytics
</CardTitle> </CardTitle>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1"> <div className="flex items-end">
Last updated: {format(new Date(basicData.lastUpdated), "HH:mm:ss")} <TooltipProvider>
<UITooltip>
<TooltipTrigger>
<div className="text-xs text-muted-foreground">
Last updated:{" "}
{format(new Date(basicData.lastUpdated), "h:mm a")}
</div> </div>
</div> </TooltipTrigger>
<div className="flex items-center space-x-4"> <TooltipContent className="p-3">
<QuotaInfo tokenQuota={basicData.tokenQuota} /> <QuotaInfo tokenQuota={basicData.tokenQuota} />
<Button </TooltipContent>
variant={isPaused ? "default" : "secondary"} </UITooltip>
size="sm" </TooltipProvider>
onClick={togglePause}
>
{isPaused ? "Resume" : "Pause"}
</Button>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-6 pt-0">
{error && ( {error && (
<Alert variant="destructive" className="mb-4"> <Alert variant="destructive" className="mb-4">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
@@ -375,16 +394,18 @@ export const RealtimeAnalytics = () => {
</Alert> </Alert>
)} )}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-1 mb-3">
{summaryCard( {summaryCard(
"Active Users",
"Last 30 minutes", "Last 30 minutes",
basicData.last30MinUsers "Active users",
basicData.last30MinUsers,
{ colorClass: METRIC_COLORS.activeUsers.className }
)} )}
{summaryCard( {summaryCard(
"Recent Activity",
"Last 5 minutes", "Last 5 minutes",
basicData.last5MinUsers "Active users",
basicData.last5MinUsers,
{ colorClass: METRIC_COLORS.activeUsers.className }
)} )}
</div> </div>
@@ -396,40 +417,58 @@ export const RealtimeAnalytics = () => {
</TabsList> </TabsList>
<TabsContent value="activity"> <TabsContent value="activity">
<div className="h-[300px]"> <div className="h-[235px] bg-card rounded-lg">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart data={basicData.byMinute}> <BarChart
data={basicData.byMinute}
margin={{ top: 5, right: 5, left: -35, bottom: -5 }}
>
<XAxis <XAxis
dataKey="minute" dataKey="minute"
tickFormatter={(value) => value + "m ago"} tickFormatter={(value) => value + "m"}
className="text-gray-600 dark:text-gray-300" className="text-xs"
tick={{ fill: "currentColor" }}
/> />
<YAxis className="text-gray-600 dark:text-gray-300" /> <YAxis className="text-xs" tick={{ fill: "currentColor" }} />
<Tooltip <Tooltip
content={({ active, payload }) => { content={({ active, payload }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const timestamp = new Date(
Date.now() + payload[0].payload.minute * 60000
);
return ( return (
<div className="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-lg shadow-lg p-3"> <Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100"> <CardContent className="p-0 space-y-2">
{payload[0].payload.minute}m ago <p className="font-medium text-sm border-b pb-1 mb-2">
</p> {format(timestamp, "h:mm a")}
<p className="text-sm text-gray-600 dark:text-gray-300">
{payload[0].value} active users
</p> </p>
<div className="flex justify-between items-center text-sm">
<span
style={{
color: METRIC_COLORS.activeUsers.color,
}}
>
Active Users:
</span>
<span className="font-medium ml-4">
{payload[0].value.toLocaleString()}
</span>
</div> </div>
</CardContent>
</Card>
); );
} }
return null; return null;
}} }}
/> />
<Bar dataKey="users" fill="#8b5cf6" /> <Bar dataKey="users" fill={METRIC_COLORS.activeUsers.color} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="pages"> <TabsContent value="pages">
<div className="space-y-2 h-[300px] 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"> <div className="space-y-2 h-[230px] 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">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="dark:border-gray-800"> <TableRow className="dark:border-gray-800">
@@ -447,7 +486,9 @@ export const RealtimeAnalytics = () => {
<TableCell className="font-medium text-gray-900 dark:text-gray-100"> <TableCell className="font-medium text-gray-900 dark:text-gray-100">
{page.path} {page.path}
</TableCell> </TableCell>
<TableCell className="text-right text-gray-600 dark:text-gray-300"> <TableCell
className={`text-right ${METRIC_COLORS.pages.className}`}
>
{page.activeUsers} {page.activeUsers}
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -458,7 +499,7 @@ export const RealtimeAnalytics = () => {
</TabsContent> </TabsContent>
<TabsContent value="sources"> <TabsContent value="sources">
<div className="space-y-2 h-[300px] 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"> <div className="space-y-2 h-[230px] 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">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="dark:border-gray-800"> <TableRow className="dark:border-gray-800">
@@ -476,7 +517,9 @@ export const RealtimeAnalytics = () => {
<TableCell className="font-medium text-gray-900 dark:text-gray-100"> <TableCell className="font-medium text-gray-900 dark:text-gray-100">
{source.source} {source.source}
</TableCell> </TableCell>
<TableCell className="text-right text-gray-600 dark:text-gray-300"> <TableCell
className={`text-right ${METRIC_COLORS.sources.className}`}
>
{source.activeUsers} {source.activeUsers}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -193,7 +193,7 @@ export const UserBehaviorDashboard = () => {
}; };
return ( return (
<Card className="bg-white dark:bg-gray-900"> <Card className="bg-white dark:bg-gray-900 h-full">
<CardHeader> <CardHeader>
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100"> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
@@ -227,7 +227,7 @@ export const UserBehaviorDashboard = () => {
<TabsContent <TabsContent
value="pages" 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" className="mt-4 space-y-2 h-full max-h-[540px] 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> <Table>
<TableHeader> <TableHeader>
@@ -269,7 +269,7 @@ export const UserBehaviorDashboard = () => {
<TabsContent <TabsContent
value="sources" 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" className="mt-4 space-y-2 h-full max-h-[540px] 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> <Table>
<TableHeader> <TableHeader>
@@ -311,7 +311,7 @@ export const UserBehaviorDashboard = () => {
<TabsContent <TabsContent
value="devices" 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" className="mt-4 space-y-2 h-full max-h-[540px] 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"> <div className="h-60">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">