Adjust app/component layouts, restyle analyticsdashboard and realtimeanalytics
This commit is contained in:
@@ -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,25 +102,35 @@ 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">
|
||||||
<KlaviyoCampaigns />
|
<div id="product-grid" className="col-span-4 h-[500px]">
|
||||||
|
<ProductGrid />
|
||||||
|
</div>
|
||||||
|
<div id="campaigns" className="col-span-8">
|
||||||
|
<KlaviyoCampaigns />
|
||||||
|
</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>
|
||||||
<div id="meta-campaigns">
|
<div id="meta-campaigns">
|
||||||
<MetaCampaigns />
|
<MetaCampaigns />
|
||||||
|
|||||||
@@ -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,322 +193,273 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return { totals, averages };
|
||||||
};
|
};
|
||||||
|
|
||||||
const summary = calculateSummary();
|
const summaryStats = calculateSummaryStats();
|
||||||
|
|
||||||
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 }) => {
|
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">
|
||||||
{label instanceof Date ? label.toLocaleDateString() : label}
|
<p className="font-medium text-sm border-b pb-1 mb-2">
|
||||||
</p>
|
{label instanceof Date ? label.toLocaleDateString() : label}
|
||||||
{payload.map((entry, index) => (
|
|
||||||
<p key={index} className="text-sm" style={{ color: entry.color }}>
|
|
||||||
{`${entry.name}: ${Number(entry.value).toLocaleString()}`}
|
|
||||||
</p>
|
</p>
|
||||||
))}
|
<div className="space-y-1">
|
||||||
</div>
|
{payload.map((entry, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
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>
|
||||||
|
</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 justify-between items-start">
|
<div className="flex flex-col space-y-2">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<div className="flex justify-between items-start">
|
||||||
Analytics Overview
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
</CardTitle>
|
Analytics Overview
|
||||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
</CardTitle>
|
||||||
<SelectTrigger className="w-36 bg-white dark:bg-gray-800">
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
<SelectValue placeholder="Select range" />
|
<SelectTrigger className="w-[130px] h-9">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select range" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectItem value="7">Last 7 days</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="14">Last 14 days</SelectItem>
|
<SelectItem value="7">Last 7 days</SelectItem>
|
||||||
<SelectItem value="30">Last 30 days</SelectItem>
|
<SelectItem value="14">Last 14 days</SelectItem>
|
||||||
<SelectItem value="90">Last 90 days</SelectItem>
|
<SelectItem value="30">Last 30 days</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value="90">Last 90 days</SelectItem>
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
|
</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
|
||||||
</div>
|
title="Active Users"
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
value={summaryStats.totals.activeUsers.toLocaleString()}
|
||||||
{summary.activeUsers.toLocaleString()}
|
description={`Avg: ${Math.round(
|
||||||
</div>
|
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="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
|
) : null}
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
|
<div className="flex items-center flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<Button
|
||||||
|
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 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<SkeletonChart />
|
||||||
<LineChart data={data}>
|
) : !data.length ? (
|
||||||
<CartesianGrid
|
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
|
||||||
strokeDasharray="3 3"
|
<div className="text-center">
|
||||||
className="stroke-gray-200 dark:stroke-gray-700"
|
<TrendingUp className="h-12 w-12 mx-auto mb-4" />
|
||||||
/>
|
<div className="font-medium mb-2">No analytics data available</div>
|
||||||
<XAxis
|
<div className="text-sm">Try selecting a different time range</div>
|
||||||
dataKey="date"
|
</div>
|
||||||
tickFormatter={formatXAxisDate}
|
</div>
|
||||||
className="text-gray-600 dark:text-gray-300"
|
) : (
|
||||||
/>
|
<div className="h-[400px] mt-4 bg-card rounded-lg p-0">
|
||||||
<YAxis
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
yAxisId="left"
|
<LineChart
|
||||||
className="text-gray-600 dark:text-gray-300"
|
data={data}
|
||||||
/>
|
margin={{ top: 5, right: 0, left: -5, bottom: 5 }}
|
||||||
<YAxis
|
>
|
||||||
yAxisId="right"
|
<CartesianGrid
|
||||||
orientation="right"
|
strokeDasharray="3 3"
|
||||||
className="text-gray-600 dark:text-gray-300"
|
className="stroke-muted"
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<XAxis
|
||||||
<Legend content={<CustomLegend metrics={metrics} selectedMetrics={selectedMetrics} />} />
|
dataKey="date"
|
||||||
{selectedMetrics.activeUsers && (
|
tickFormatter={formatXAxis}
|
||||||
<Line
|
className="text-xs"
|
||||||
|
tick={{ fill: "currentColor" }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
yAxisId="left"
|
yAxisId="left"
|
||||||
type="monotone"
|
className="text-xs"
|
||||||
dataKey="activeUsers"
|
tick={{ fill: "currentColor" }}
|
||||||
name="Active Users"
|
|
||||||
stroke={metrics.activeUsers.color}
|
|
||||||
dot={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<YAxis
|
||||||
{selectedMetrics.newUsers && (
|
|
||||||
<Line
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="newUsers"
|
|
||||||
name="New Users"
|
|
||||||
stroke={metrics.newUsers.color}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedMetrics.pageViews && (
|
|
||||||
<Line
|
|
||||||
yAxisId="right"
|
yAxisId="right"
|
||||||
type="monotone"
|
orientation="right"
|
||||||
dataKey="pageViews"
|
className="text-xs"
|
||||||
name="Page Views"
|
tick={{ fill: "currentColor" }}
|
||||||
stroke={metrics.pageViews.color}
|
|
||||||
dot={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<Tooltip content={<CustomTooltip />} />
|
||||||
{selectedMetrics.conversions && (
|
<Legend />
|
||||||
<Line
|
{metrics.activeUsers && (
|
||||||
yAxisId="right"
|
<Line
|
||||||
type="monotone"
|
yAxisId="left"
|
||||||
dataKey="conversions"
|
type="monotone"
|
||||||
name="Conversions"
|
dataKey="activeUsers"
|
||||||
stroke={metrics.conversions.color}
|
name="Active Users"
|
||||||
dot={false}
|
stroke={METRIC_COLORS.activeUsers.color}
|
||||||
/>
|
strokeWidth={2}
|
||||||
)}
|
dot={false}
|
||||||
</LineChart>
|
/>
|
||||||
</ResponsiveContainer>
|
)}
|
||||||
</div>
|
{metrics.newUsers && (
|
||||||
|
<Line
|
||||||
<div className="flex flex-wrap gap-4 mt-4">
|
yAxisId="left"
|
||||||
<MetricToggle
|
type="monotone"
|
||||||
label="Active Users"
|
dataKey="newUsers"
|
||||||
checked={selectedMetrics.activeUsers}
|
name="New Users"
|
||||||
onChange={(checked) =>
|
stroke={METRIC_COLORS.newUsers.color}
|
||||||
setSelectedMetrics((prev) => ({ ...prev, activeUsers: checked }))
|
strokeWidth={2}
|
||||||
}
|
dot={false}
|
||||||
/>
|
/>
|
||||||
<MetricToggle
|
)}
|
||||||
label="New Users"
|
{metrics.pageViews && (
|
||||||
checked={selectedMetrics.newUsers}
|
<Line
|
||||||
onChange={(checked) =>
|
yAxisId="right"
|
||||||
setSelectedMetrics((prev) => ({ ...prev, newUsers: checked }))
|
type="monotone"
|
||||||
}
|
dataKey="pageViews"
|
||||||
/>
|
name="Page Views"
|
||||||
<MetricToggle
|
stroke={METRIC_COLORS.pageViews.color}
|
||||||
label="Page Views"
|
strokeWidth={2}
|
||||||
checked={selectedMetrics.pageViews}
|
dot={false}
|
||||||
onChange={(checked) =>
|
/>
|
||||||
setSelectedMetrics((prev) => ({ ...prev, pageViews: checked }))
|
)}
|
||||||
}
|
{metrics.conversions && (
|
||||||
/>
|
<Line
|
||||||
<MetricToggle
|
yAxisId="right"
|
||||||
label="Conversions"
|
type="monotone"
|
||||||
checked={selectedMetrics.conversions}
|
dataKey="conversions"
|
||||||
onChange={(checked) =>
|
name="Conversions"
|
||||||
setSelectedMetrics((prev) => ({ ...prev, conversions: checked }))
|
stroke={METRIC_COLORS.conversions.color}
|
||||||
}
|
strokeWidth={2}
|
||||||
/>
|
dot={false}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
{displayValue}
|
<div className={`text-2xl font-bold mb-1 ${colorClass}`}>
|
||||||
</div>
|
{displayValue}
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">{sublabel}</div>
|
</div>
|
||||||
</div>
|
<div className="text-sm text-muted-foreground">{sublabel}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,54 +131,51 @@ const QuotaInfo = ({ tokenQuota }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<>
|
||||||
<UITooltip>
|
<div className="flex items-center font-semibold rounded-md space-x-1">
|
||||||
<TooltipTrigger>
|
<span>Quota:</span>
|
||||||
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 space-x-1">
|
<span className={`font-medium ${getStatusColor(hourlyPercentage)}`}>
|
||||||
<span>Quota:</span>
|
{hourlyPercentage}%
|
||||||
<span className={`font-medium ${getStatusColor(hourlyPercentage)}`}>
|
</span>
|
||||||
{hourlyPercentage}%
|
</div>
|
||||||
</span>
|
|
||||||
</div>
|
<div className="dark:border-gray-700">
|
||||||
</TooltipTrigger>
|
<div className="space-y-3 mt-2">
|
||||||
<TooltipContent className="bg-white dark:bg-gray-800 border dark:border-gray-700">
|
<div>
|
||||||
<div className="space-y-3 p-2">
|
<div className="font-semibold text-gray-100">
|
||||||
<div>
|
Project Hourly
|
||||||
<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>
|
<div className={`${getStatusColor(hourlyPercentage)}`}>
|
||||||
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
{projectHourlyRemaining.toLocaleString()} / 14,000 remaining
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
<div>
|
||||||
</UITooltip>
|
<div className="font-semibold text-gray-100">
|
||||||
</TooltipProvider>
|
Daily
|
||||||
|
</div>
|
||||||
|
<div className={`${getStatusColor(dailyPercentage)}`}>
|
||||||
|
{dailyRemaining.toLocaleString()} / 200,000 remaining
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-100">
|
||||||
|
Server Errors
|
||||||
|
</div>
|
||||||
|
<div className={`${getStatusColor(errorPercentage)}`}>
|
||||||
|
{errorsConsumed} / 10 used this hour
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-100">
|
||||||
|
Thresholded Requests
|
||||||
|
</div>
|
||||||
|
<div className={`${getStatusColor(thresholdPercentage)}`}>
|
||||||
|
{thresholdConsumed} / 120 used this hour
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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="flex items-end">
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
<TooltipProvider>
|
||||||
Last updated: {format(new Date(basicData.lastUpdated), "HH:mm:ss")}
|
<UITooltip>
|
||||||
</div>
|
<TooltipTrigger>
|
||||||
</div>
|
<div className="text-xs text-muted-foreground">
|
||||||
<div className="flex items-center space-x-4">
|
Last updated:{" "}
|
||||||
<QuotaInfo tokenQuota={basicData.tokenQuota} />
|
{format(new Date(basicData.lastUpdated), "h:mm a")}
|
||||||
<Button
|
</div>
|
||||||
variant={isPaused ? "default" : "secondary"}
|
</TooltipTrigger>
|
||||||
size="sm"
|
<TooltipContent className="p-3">
|
||||||
onClick={togglePause}
|
<QuotaInfo tokenQuota={basicData.tokenQuota} />
|
||||||
>
|
</TooltipContent>
|
||||||
{isPaused ? "Resume" : "Pause"}
|
</UITooltip>
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</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">
|
</p>
|
||||||
{payload[0].value} active users
|
<div className="flex justify-between items-center text-sm">
|
||||||
</p>
|
<span
|
||||||
</div>
|
style={{
|
||||||
|
color: METRIC_COLORS.activeUsers.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Active Users:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium ml-4">
|
||||||
|
{payload[0].value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</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>
|
||||||
@@ -491,4 +534,4 @@ export const RealtimeAnalytics = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RealtimeAnalytics;
|
export default RealtimeAnalytics;
|
||||||
|
|||||||
@@ -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%">
|
||||||
|
|||||||
Reference in New Issue
Block a user