From 80107df5feca0ede09e0b46af46d803adbca1af1 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Jan 2025 14:59:14 -0500 Subject: [PATCH] Fix styling regression on main statcards component --- dashboard/src/App.jsx | 60 +++-- .../components/dashboard/MiniSalesChart.jsx | 245 ++++++++++++++++++ .../src/components/dashboard/SalesChart.jsx | 113 ++------ 3 files changed, 310 insertions(+), 108 deletions(-) create mode 100644 dashboard/src/components/dashboard/MiniSalesChart.jsx diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index e2404ca..f4ec1c1 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -29,6 +29,7 @@ import UserBehaviorDashboard from "@/components/dashboard/UserBehaviorDashboard" import TypeformDashboard from "@/components/dashboard/TypeformDashboard"; import MiniStatCards from "@/components/dashboard/MiniStatCards"; import MiniRealtimeAnalytics from "@/components/dashboard/MiniRealtimeAnalytics"; +import MiniSalesChart from "@/components/dashboard/MiniSalesChart"; // Public layout const PublicLayout = () => ( @@ -66,6 +67,7 @@ const SmallLayout = () => { const DATETIME_SCALE = 2; const STATS_SCALE = 1.65; const ANALYTICS_SCALE = 1.65; + const SALES_SCALE = 1.65; return (
@@ -81,31 +83,51 @@ const SmallLayout = () => { transformOrigin: 'top left', width: `${100/DATETIME_SCALE}%` }}> - +
{/* Stats and Analytics */}
-
-
- +
+ {/* Mini Stat Cards */} +
+
+ +
-
-
-
- + + {/* Mini Charts Grid */} +
+ {/* Mini Sales Chart */} +
+
+ +
+
+ + {/* Mini Realtime Analytics */} +
+
+ +
+
diff --git a/dashboard/src/components/dashboard/MiniSalesChart.jsx b/dashboard/src/components/dashboard/MiniSalesChart.jsx new file mode 100644 index 0000000..0f5853a --- /dev/null +++ b/dashboard/src/components/dashboard/MiniSalesChart.jsx @@ -0,0 +1,245 @@ +import React, { useState, useEffect, useCallback, memo } from "react"; +import axios from "axios"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { DateTime } from "luxon"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { AlertCircle, TrendingUp, DollarSign, ShoppingCart } from "lucide-react"; +import { formatCurrency, CustomTooltip, processData, StatCard } from "./SalesChart.jsx"; + +const SkeletonChart = () => ( +
+
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+
+ {[...Array(6)].map((_, i) => ( + + ))} +
+
+
+
+
+
+
+
+); + +const MiniStatCard = memo(({ title, value, icon: Icon, colorClass, iconColor, iconBackground, background }) => ( + + + + {title} + + {Icon && ( +
+
+ +
+ )} + + +
+ {value} +
+
+ +)); + +MiniStatCard.displayName = "MiniStatCard"; + +const MiniSalesChart = ({ className = "" }) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [summaryStats, setSummaryStats] = useState({ + totalRevenue: 0, + totalOrders: 0, + }); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const response = await axios.get("/api/klaviyo/events/stats/details", { + params: { + timeRange: "last30days", + metric: "revenue", + daily: true, + }, + }); + + if (!response.data) { + throw new Error("Invalid response format"); + } + + const stats = Array.isArray(response.data) + ? response.data + : response.data.stats || []; + + const processedData = processData(stats); + + // Calculate totals + const totals = stats.reduce((acc, day) => ({ + totalRevenue: acc.totalRevenue + (Number(day.revenue) || 0), + totalOrders: acc.totalOrders + (Number(day.orders) || 0), + }), { totalRevenue: 0, totalOrders: 0 }); + + setData(processedData); + setSummaryStats(totals); + setError(null); + } catch (error) { + console.error("Error fetching data:", error); + setError(error.message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + const intervalId = setInterval(fetchData, 300000); + return () => clearInterval(intervalId); + }, [fetchData]); + + const formatXAxis = (value) => { + if (!value) return ""; + const date = new Date(value); + return date.toLocaleDateString([], { + month: "short", + day: "numeric" + }); + }; + + if (error) { + return ( + + + Error + Failed to load sales data: {error} + + ); + } + + return ( +
+ {/* Stat Cards */} +
+ + +
+ + {/* Chart */} + {loading ? ( + + ) : !data.length ? ( +
+
+ +
No sales data available
+
+
+ ) : ( +
+ + + + + formatCurrency(value, false)} + className="text-[10px] text-gray-300" + tick={{ fill: "currentColor" }} + /> + value.toLocaleString()} + className="text-[10px] text-gray-300" + tick={{ fill: "currentColor" }} + /> + } /> + + + + +
+ )} +
+ ); +}; + +export default MiniSalesChart; \ No newline at end of file diff --git a/dashboard/src/components/dashboard/SalesChart.jsx b/dashboard/src/components/dashboard/SalesChart.jsx index eedbeb5..98bbedb 100644 --- a/dashboard/src/components/dashboard/SalesChart.jsx +++ b/dashboard/src/components/dashboard/SalesChart.jsx @@ -109,16 +109,15 @@ const calculatePreviousPeriodDates = (timeRange, startDate, endDate) => { return null; }; -// Enhanced helper function for consistent currency formatting with explicit rounding -const formatCurrency = (value, useFractionDigits = true) => { - if (typeof value !== "number") return "$0.00"; - const roundedValue = parseFloat(value.toFixed(useFractionDigits ? 2 : 0)); +// Move formatCurrency to top and export it +export const formatCurrency = (value, minimumFractionDigits = 0) => { + if (!value || isNaN(value)) return "$0"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", - minimumFractionDigits: useFractionDigits ? 2 : 0, - maximumFractionDigits: useFractionDigits ? 2 : 0, - }).format(roundedValue); + minimumFractionDigits, + maximumFractionDigits: minimumFractionDigits, + }).format(value); }; // Add a helper function for percentage formatting @@ -139,7 +138,7 @@ const METRIC_COLORS = { }; // Memoize the StatCard component -const StatCard = memo( +export const StatCard = memo( ({ title, value, @@ -189,99 +188,34 @@ const StatCard = memo( ) ); -// Add display name for debugging StatCard.displayName = "StatCard"; -const CustomTooltip = ({ active, payload, label }) => { +// Export CustomTooltip +export const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { const date = new Date(label); const formattedDate = date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", - year: "numeric", }); - // Group metrics by type (current vs previous) - const currentMetrics = payload.filter( - (p) => !p.dataKey.toLowerCase().includes("prev") - ); - const previousMetrics = payload.filter((p) => - p.dataKey.toLowerCase().includes("prev") - ); - return ( - - -

- {formattedDate} -

+ + +

{formattedDate}

+ {payload.map((entry, index) => { + const value = entry.dataKey.toLowerCase().includes('revenue') || entry.dataKey === 'avgOrderValue' + ? formatCurrency(entry.value) + : entry.value.toLocaleString(); -
- {currentMetrics.map((entry, index) => { - const value = - entry.dataKey.toLowerCase().includes("revenue") || - entry.dataKey === "avgOrderValue" || - entry.dataKey === "movingAverage" || - entry.dataKey === "aovMovingAverage" - ? formatCurrency(entry.value) - : entry.value.toLocaleString(); - - return ( -
- - {entry.name}: - - {value} -
- ); - })} -
- - {previousMetrics.length > 0 && ( - <> -
-
-

- Previous Period -

- {previousMetrics.map((entry, index) => { - const value = - entry.dataKey.toLowerCase().includes("revenue") || - entry.dataKey.includes("avgOrderValue") - ? formatCurrency(entry.value) - : entry.value.toLocaleString(); - - return ( -
- - {entry.name.replace("Previous ", "")}: - - {value} -
- ); - })} + return ( +
+ {entry.name}: + {value}
- - )} + ); + })} ); @@ -332,7 +266,8 @@ const calculate7DayAverage = (data) => { }); }; -const processData = (stats = []) => { +// Export processData +export const processData = (stats = []) => { if (!Array.isArray(stats)) return []; // First, convert the stats array into the base format