diff --git a/dashboard/src/components/dashboard/AnalyticsDashboard.jsx b/dashboard/src/components/dashboard/AnalyticsDashboard.jsx
index 0ad2d6a..ef86fcb 100644
--- a/dashboard/src/components/dashboard/AnalyticsDashboard.jsx
+++ b/dashboard/src/components/dashboard/AnalyticsDashboard.jsx
@@ -7,7 +7,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { Checkbox } from "@/components/ui/checkbox";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
import {
LineChart,
Line,
@@ -17,13 +18,130 @@ import {
Tooltip,
Legend,
ResponsiveContainer,
+ ReferenceLine,
} 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 = () => (
+
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+
+
+
+);
+
+const SkeletonStats = () => (
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+);
+
+// Add StatCard component
+const StatCard = ({
+ title,
+ value,
+ description,
+ trend,
+ trendValue,
+ colorClass = "text-gray-900 dark:text-gray-100",
+}) => (
+
+
+ {title}
+ {trend && (
+
+ {trendValue}
+
+ )}
+
+
+ {value}
+ {description && (
+ {description}
+ )}
+
+
+);
+
+// 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 = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState("30");
+ const [metrics, setMetrics] = useState({
+ activeUsers: true,
+ newUsers: true,
+ pageViews: true,
+ conversions: true,
+ });
useEffect(() => {
const fetchData = async () => {
@@ -75,322 +193,273 @@ export const AnalyticsDashboard = () => {
return new Date(year, month - 1, day);
};
- const [selectedMetrics, setSelectedMetrics] = useState({
- activeUsers: true,
- newUsers: true,
- pageViews: true,
- conversions: true,
- });
-
- const MetricToggle = ({ label, checked, onChange }) => (
-
-
-
-
- );
-
- 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 (
-
-
-
- Left Axis
-
- {leftAxisItems.map(([key, metric]) => (
-
- ))}
-
-
-
- Right Axis
-
- {rightAxisItems.map(([key, metric]) => (
-
- ))}
-
-
- );
+ const formatXAxis = (date) => {
+ if (!date) return "";
+ return date.toLocaleDateString([], { month: "short", day: "numeric" });
};
- const metrics = {
- 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 = () => {
+ const calculateSummaryStats = () => {
if (!data.length) return null;
const totals = data.reduce(
(acc, day) => ({
- activeUsers: acc.activeUsers + (Number(day.activeUsers) || 0),
- newUsers: acc.newUsers + (Number(day.newUsers) || 0),
- pageViews: acc.pageViews + (Number(day.pageViews) || 0),
- conversions: acc.conversions + (Number(day.conversions) || 0),
- avgSessionDuration:
- acc.avgSessionDuration + (Number(day.avgSessionDuration) || 0),
- bounceRate: acc.bounceRate + (Number(day.bounceRate) || 0),
+ activeUsers: acc.activeUsers + day.activeUsers,
+ newUsers: acc.newUsers + day.newUsers,
+ pageViews: acc.pageViews + day.pageViews,
+ conversions: acc.conversions + day.conversions,
}),
{
activeUsers: 0,
newUsers: 0,
pageViews: 0,
conversions: 0,
- avgSessionDuration: 0,
- bounceRate: 0,
}
);
- return {
- ...totals,
- avgSessionDuration: totals.avgSessionDuration / data.length,
- bounceRate: totals.bounceRate / data.length,
+ const averages = {
+ activeUsers: totals.activeUsers / data.length,
+ newUsers: totals.newUsers / data.length,
+ pageViews: totals.pageViews / data.length,
+ conversions: totals.conversions / data.length,
};
+
+ return { totals, averages };
};
- const summary = calculateSummary();
-
- if (loading) {
- return (
-
-
-
-
-
- );
- }
-
- const formatXAxisDate = (date) => {
- if (!(date instanceof Date)) return "";
- return `${date.getMonth() + 1}/${date.getDate()}`;
- };
+ const summaryStats = calculateSummaryStats();
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
-
-
- {label instanceof Date ? label.toLocaleDateString() : label}
-
- {payload.map((entry, index) => (
-
- {`${entry.name}: ${Number(entry.value).toLocaleString()}`}
+
+
+
+ {label instanceof Date ? label.toLocaleDateString() : label}
- ))}
-
+
+ {payload.map((entry, index) => (
+
+ {entry.name}:
+
+ {entry.value.toLocaleString()}
+
+
+ ))}
+
+
+
);
}
return null;
};
return (
-
-
-
-
- Analytics Overview
-
-
-
+
+
+
+
+
+ Analytics Overview
+
+
+
- {summary && (
-
-
-
- Total Users
-
-
- {summary.activeUsers.toLocaleString()}
-
+ {loading ? (
+
+ ) : summaryStats ? (
+
+
+
+
+
-
-
+ ) : null}
+
+
+
+
+
-
- {summary.newUsers.toLocaleString()}
-
-
-
-
+
+
-
- {summary.pageViews.toLocaleString()}
-
-
-
-
+
+
-
- {summary.conversions.toLocaleString()}
-
+
- )}
+
-
-
-
-
-
-
-
-
- } />
- } />
- {selectedMetrics.activeUsers && (
-
+ {loading ? (
+
+ ) : !data.length ? (
+
+
+
+
No analytics data available
+
Try selecting a different time range
+
+
+ ) : (
+
+
+
+
+
+
- )}
- {selectedMetrics.newUsers && (
-
- )}
- {selectedMetrics.pageViews && (
-
- )}
- {selectedMetrics.conversions && (
-
- )}
-
-
-
-
-
-
- setSelectedMetrics((prev) => ({ ...prev, activeUsers: checked }))
- }
- />
-
- setSelectedMetrics((prev) => ({ ...prev, newUsers: checked }))
- }
- />
-
- setSelectedMetrics((prev) => ({ ...prev, pageViews: checked }))
- }
- />
-
- setSelectedMetrics((prev) => ({ ...prev, conversions: checked }))
- }
- />
-
+ } />
+
+ {metrics.activeUsers && (
+
+ )}
+ {metrics.newUsers && (
+
+ )}
+ {metrics.pageViews && (
+
+ )}
+ {metrics.conversions && (
+
+ )}
+
+
+
+ )}
);
diff --git a/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx b/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx
index 928e4a7..7617f48 100644
--- a/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx
+++ b/dashboard/src/components/dashboard/KlaviyoCampaigns.jsx
@@ -219,7 +219,7 @@ const KlaviyoCampaigns = ({ className }) => {
-
+
diff --git a/dashboard/src/components/dashboard/RealtimeAnalytics.jsx b/dashboard/src/components/dashboard/RealtimeAnalytics.jsx
index dca6369..c9d2d08 100644
--- a/dashboard/src/components/dashboard/RealtimeAnalytics.jsx
+++ b/dashboard/src/components/dashboard/RealtimeAnalytics.jsx
@@ -31,6 +31,21 @@ import {
} from "@/components/ui/table";
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) => {
return new Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
@@ -46,6 +61,7 @@ const summaryCard = (label, sublabel, value, options = {}) => {
isMonetary = false,
isPercentage = false,
decimalPlaces = 0,
+ colorClass = METRIC_COLORS.activeUsers.className,
} = options;
let displayValue;
@@ -58,15 +74,17 @@ const summaryCard = (label, sublabel, value, options = {}) => {
}
return (
-
-
- {label}
-
-
- {displayValue}
-
-
{sublabel}
-
+
+
+ {label}
+
+
+
+ {displayValue}
+
+ {sublabel}
+
+
);
};
@@ -113,54 +131,51 @@ const QuotaInfo = ({ tokenQuota }) => {
};
return (
-
-
-
-
- Quota:
-
- {hourlyPercentage}%
-
-
-
-
-
-
-
- Project Hourly
-
-
- {projectHourlyRemaining.toLocaleString()} / 14,000 remaining
-
+ <>
+
+ Quota:
+
+ {hourlyPercentage}%
+
+
+
+
+
+
+
+ Project Hourly
-
-
- Daily
-
-
- {dailyRemaining.toLocaleString()} / 200,000 remaining
-
-
-
-
- Server Errors
-
-
- {errorsConsumed} / 10 used this hour
-
-
-
-
- Thresholded Requests
-
-
- {thresholdConsumed} / 120 used this hour
-
+
+ {projectHourlyRemaining.toLocaleString()} / 14,000 remaining
-
-
-
+
+
+ Daily
+
+
+ {dailyRemaining.toLocaleString()} / 200,000 remaining
+
+
+
+
+ Server Errors
+
+
+ {errorsConsumed} / 10 used this hour
+
+
+
+
+ Thresholded Requests
+
+
+ {thresholdConsumed} / 120 used this hour
+
+
+
+
+ >
);
};
@@ -195,7 +210,9 @@ export const RealtimeAnalytics = () => {
const matchingRow = data.timeSeriesResponse?.rows?.find(
(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);
return {
minute: -i,
@@ -243,7 +260,9 @@ export const RealtimeAnalytics = () => {
data.eventResponse?.rows
?.filter(
(row) =>
- !["session_start", "(other)"].includes(row.dimensionValues[0].value)
+ !["session_start", "(other)"].includes(
+ row.dimensionValues[0].value
+ )
)
.map((row) => ({
event: row.dimensionValues[0].value,
@@ -343,31 +362,31 @@ export const RealtimeAnalytics = () => {
}
return (
-
-
-
-
-
- Real-Time Analytics
-
-
- Last updated: {format(new Date(basicData.lastUpdated), "HH:mm:ss")}
-
-
-
-
-
+
+
+
+
+ Real-Time Analytics
+
+
+
+
+
+
+ Last updated:{" "}
+ {format(new Date(basicData.lastUpdated), "h:mm a")}
+
+
+
+
+
+
+
-
+
{error && (
@@ -375,16 +394,18 @@ export const RealtimeAnalytics = () => {
)}
-
+
{summaryCard(
- "Active Users",
"Last 30 minutes",
- basicData.last30MinUsers
+ "Active users",
+ basicData.last30MinUsers,
+ { colorClass: METRIC_COLORS.activeUsers.className }
)}
{summaryCard(
- "Recent Activity",
"Last 5 minutes",
- basicData.last5MinUsers
+ "Active users",
+ basicData.last5MinUsers,
+ { colorClass: METRIC_COLORS.activeUsers.className }
)}
@@ -396,40 +417,58 @@ export const RealtimeAnalytics = () => {
-
+
-
+
value + "m ago"}
- className="text-gray-600 dark:text-gray-300"
+ tickFormatter={(value) => value + "m"}
+ className="text-xs"
+ tick={{ fill: "currentColor" }}
/>
-
+
{
if (active && payload && payload.length) {
+ const timestamp = new Date(
+ Date.now() + payload[0].payload.minute * 60000
+ );
return (
-
-
- {payload[0].payload.minute}m ago
-
-
- {payload[0].value} active users
-
-
+
+
+
+ {format(timestamp, "h:mm a")}
+
+
+
+ Active Users:
+
+
+ {payload[0].value.toLocaleString()}
+
+
+
+
);
}
return null;
}}
/>
-
+
-
+
@@ -447,7 +486,9 @@ export const RealtimeAnalytics = () => {
{page.path}
-
+
{page.activeUsers}
@@ -458,7 +499,7 @@ export const RealtimeAnalytics = () => {
-
+
@@ -476,7 +517,9 @@ export const RealtimeAnalytics = () => {
{source.source}
-
+
{source.activeUsers}
@@ -491,4 +534,4 @@ export const RealtimeAnalytics = () => {
);
};
-export default RealtimeAnalytics;
\ No newline at end of file
+export default RealtimeAnalytics;
diff --git a/dashboard/src/components/dashboard/UserBehaviorDashboard.jsx b/dashboard/src/components/dashboard/UserBehaviorDashboard.jsx
index 88cf24e..b652bc2 100644
--- a/dashboard/src/components/dashboard/UserBehaviorDashboard.jsx
+++ b/dashboard/src/components/dashboard/UserBehaviorDashboard.jsx
@@ -193,7 +193,7 @@ export const UserBehaviorDashboard = () => {
};
return (
-
+
@@ -227,7 +227,7 @@ export const UserBehaviorDashboard = () => {
@@ -269,7 +269,7 @@ export const UserBehaviorDashboard = () => {
@@ -311,7 +311,7 @@ export const UserBehaviorDashboard = () => {