Fix styling regression on main statcards component

This commit is contained in:
2025-01-02 14:59:14 -05:00
parent 925eda8677
commit 80107df5fe
3 changed files with 310 additions and 108 deletions

View File

@@ -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 (
<div className="min-h-screen w-screen">
@@ -81,31 +83,51 @@ const SmallLayout = () => {
transformOrigin: 'top left',
width: `${100/DATETIME_SCALE}%`
}}>
<DateTimeWeatherDisplay />
<DateTimeWeatherDisplay scaleFactor={DATETIME_SCALE} />
</div>
</div>
{/* Stats and Analytics */}
<div className="col-span-9">
<div>
<div style={{
transform: `scale(${STATS_SCALE})`,
transformOrigin: 'top left',
width: `${100/STATS_SCALE}%`
}}>
<MiniStatCards
title="Live Stats"
timeRange="today"
/>
<div className="">
{/* Mini Stat Cards */}
<div>
<div style={{
transform: `scale(${STATS_SCALE})`,
transformOrigin: 'top left',
width: `${100/STATS_SCALE}%`
}}>
<MiniStatCards
title="Live Stats"
timeRange="today"
/>
</div>
</div>
</div>
<div className="w-1/2 ml-auto mt-28 pl-1">
<div style={{
transform: `scale(${ANALYTICS_SCALE})`,
transformOrigin: 'top left',
width: `${100/ANALYTICS_SCALE}%`
}}>
<MiniRealtimeAnalytics />
{/* Mini Charts Grid */}
<div className="grid grid-cols-2 gap-4 mt-28">
{/* Mini Sales Chart */}
<div>
<div style={{
transform: `scale(${SALES_SCALE})`,
transformOrigin: 'top left',
width: `${100/SALES_SCALE}%`
}}>
<MiniSalesChart />
</div>
</div>
{/* Mini Realtime Analytics */}
<div>
<div style={{
transform: `scale(${ANALYTICS_SCALE})`,
transformOrigin: 'top left',
width: `${100/ANALYTICS_SCALE}%`,
height: `${100/ANALYTICS_SCALE}%`
}}>
<MiniRealtimeAnalytics />
</div>
</div>
</div>
</div>
</div>

View File

@@ -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 = () => (
<div className="h-[200px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-4">
<div className="h-full relative">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-muted"
style={{ top: `${(i + 1) * 20}%` }}
/>
))}
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-3 w-6 bg-muted rounded-sm" />
))}
</div>
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-3 w-8 bg-muted rounded-sm" />
))}
</div>
<div className="absolute inset-x-8 bottom-6 top-4">
<div className="h-full w-full relative">
<div
className="absolute inset-0 bg-muted rounded-sm"
style={{
opacity: 0.5,
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
}}
/>
</div>
</div>
</div>
</div>
);
const MiniStatCard = memo(({ title, value, icon: Icon, colorClass, iconColor, iconBackground, background }) => (
<Card className={`w-full ${background || 'bg-gradient-to-br from-gray-800 to-gray-900 backdrop-blur-md'}`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
<CardTitle className="text-sm font-bold text-gray-100">
{title}
</CardTitle>
{Icon && (
<div className="relative p-3">
<div className={`absolute inset-0 rounded-full ${iconBackground}`} />
<Icon className={`h-4 w-4 ${iconColor} relative`} />
</div>
)}
</CardHeader>
<CardContent className="p-4 pt-0">
<div className={`text-3xl font-extrabold ${colorClass}`}>
{value}
</div>
</CardContent>
</Card>
));
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 (
<Alert variant="destructive" className="bg-white/10 backdrop-blur-sm">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load sales data: {error}</AlertDescription>
</Alert>
);
}
return (
<div>
{/* Stat Cards */}
<div className="grid grid-cols-2 gap-2">
<MiniStatCard
title="30 Days Revenue"
value={formatCurrency(summaryStats.totalRevenue, false)}
colorClass="text-emerald-200"
icon={DollarSign}
iconColor="text-emerald-900"
iconBackground="bg-emerald-300"
background="bg-gradient-to-br from-emerald-900 to-emerald-800"
/>
<MiniStatCard
title="30 Days Orders"
value={summaryStats.totalOrders.toLocaleString()}
colorClass="text-blue-200"
icon={ShoppingCart}
iconColor="text-blue-900"
iconBackground="bg-blue-300"
background="bg-gradient-to-br from-blue-900 to-blue-800"
/>
</div>
{/* Chart */}
{loading ? (
<SkeletonChart />
) : !data.length ? (
<div className="flex items-center justify-center h-[200px] text-muted-foreground">
<div className="text-center">
<TrendingUp className="h-8 w-8 mx-auto mb-2" />
<div className="text-sm text-gray-300">No sales data available</div>
</div>
</div>
) : (
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={data}
margin={{ top: 5, right: 5, left: -20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-700" />
<XAxis
dataKey="timestamp"
tickFormatter={formatXAxis}
className="text-[10px] text-gray-300"
tick={{ fill: "currentColor" }}
/>
<YAxis
yAxisId="revenue"
tickFormatter={(value) => formatCurrency(value, false)}
className="text-[10px] text-gray-300"
tick={{ fill: "currentColor" }}
/>
<YAxis
yAxisId="orders"
orientation="right"
tickFormatter={(value) => value.toLocaleString()}
className="text-[10px] text-gray-300"
tick={{ fill: "currentColor" }}
/>
<Tooltip content={<CustomTooltip />} />
<Line
yAxisId="revenue"
type="monotone"
dataKey="revenue"
name="Revenue"
stroke="#8b5cf6"
strokeWidth={2}
dot={false}
/>
<Line
yAxisId="orders"
type="monotone"
dataKey="orders"
name="Orders"
stroke="#10b981"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
);
};
export default MiniSalesChart;

View File

@@ -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 (
<Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
<CardContent className="p-0 space-y-2">
<p className="font-medium text-sm border-b pb-1 mb-2">
{formattedDate}
</p>
<Card className="p-2 shadow-lg bg-white dark:bg-gray-800 border-none">
<CardContent className="p-0 space-y-1">
<p className="font-medium text-xs">{formattedDate}</p>
{payload.map((entry, index) => {
const value = entry.dataKey.toLowerCase().includes('revenue') || entry.dataKey === 'avgOrderValue'
? formatCurrency(entry.value)
: entry.value.toLocaleString();
<div className="space-y-1">
{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 (
<div
key={index}
className="flex justify-between items-center text-sm"
>
<span
style={{
color:
entry.stroke ||
METRIC_COLORS[entry.dataKey.toLowerCase()],
}}
>
{entry.name}:
</span>
<span className="font-medium ml-4">{value}</span>
</div>
);
})}
</div>
{previousMetrics.length > 0 && (
<>
<div className="border-t my-2"></div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground mb-1">
Previous Period
</p>
{previousMetrics.map((entry, index) => {
const value =
entry.dataKey.toLowerCase().includes("revenue") ||
entry.dataKey.includes("avgOrderValue")
? formatCurrency(entry.value)
: entry.value.toLocaleString();
return (
<div
key={index}
className="flex justify-between items-center text-sm"
>
<span
style={{
color:
entry.stroke ||
METRIC_COLORS[entry.dataKey.toLowerCase()],
}}
>
{entry.name.replace("Previous ", "")}:
</span>
<span className="font-medium ml-4">{value}</span>
</div>
);
})}
return (
<div key={index} className="flex justify-between items-center text-xs gap-3">
<span style={{ color: entry.stroke }}>{entry.name}:</span>
<span className="font-medium">{value}</span>
</div>
</>
)}
);
})}
</CardContent>
</Card>
);
@@ -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