From 7291221154f44eef86ec18150e833389407be6cc Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 28 Dec 2024 00:46:06 -0500 Subject: [PATCH] Adjust app/component layouts, restyle analyticsdashboard and realtimeanalytics --- dashboard/src/App.jsx | 29 +- .../dashboard/AnalyticsDashboard.jsx | 621 ++++++++++-------- .../components/dashboard/KlaviyoCampaigns.jsx | 2 +- .../dashboard/RealtimeAnalytics.jsx | 245 ++++--- .../dashboard/UserBehaviorDashboard.jsx | 8 +- 5 files changed, 512 insertions(+), 393 deletions(-) diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index 4f7d027..b279a5e 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -93,9 +93,6 @@ const DashboardLayout = () => {
- - -
@@ -105,25 +102,35 @@ const DashboardLayout = () => {
-
+
-
+
-
+
-
- -
-
- +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
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]) => ( -
-
- - {metric.label} - -
- ))} -
-
-

- Right Axis -

- {rightAxisItems.map(([key, metric]) => ( -
-
- - {metric.label} - -
- ))} -
-
- ); + 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 = () => {