Fix grid spacing and fix statcards dialogs
This commit is contained in:
@@ -63,34 +63,33 @@ const PinProtectedLayout = ({ children }) => {
|
|||||||
// Small Layout
|
// Small Layout
|
||||||
const SmallLayout = () => {
|
const SmallLayout = () => {
|
||||||
const DATETIME_SCALE = 2;
|
const DATETIME_SCALE = 2;
|
||||||
const STATS_SCALE = 1.5;
|
const STATS_SCALE = 1.6;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-screen overflow-hidden">
|
<div className="min-h-screen w-screen">
|
||||||
<div className="flex flex-col">
|
<span className="absolute top-4 left-4 z-50">
|
||||||
<span className="absolute top-4 left-4 z-50">
|
<LockButton />
|
||||||
<LockButton />
|
</span>
|
||||||
</span>
|
|
||||||
|
<div className="p-4 grid grid-cols-12 gap-4">
|
||||||
<div className="p-4 space-y-4 grid grid-cols-12 gap-0">
|
<div className="col-span-3 relative">
|
||||||
<div
|
<div
|
||||||
className="col-span-4"
|
className="origin-top-left"
|
||||||
style={{
|
style={{
|
||||||
transform: `scale(${DATETIME_SCALE})`,
|
transform: `scale(${DATETIME_SCALE})`,
|
||||||
transformOrigin: "top left",
|
|
||||||
width: `${100/DATETIME_SCALE}%`,
|
width: `${100/DATETIME_SCALE}%`,
|
||||||
height: `${100/DATETIME_SCALE}%`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DateTimeWeatherDisplay />
|
<DateTimeWeatherDisplay />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-9 relative">
|
||||||
<div
|
<div
|
||||||
className="col-span-8"
|
className="origin-top-left"
|
||||||
style={{
|
style={{
|
||||||
transform: `scale(${STATS_SCALE})`,
|
transform: `scale(${STATS_SCALE})`,
|
||||||
transformOrigin: "top left",
|
|
||||||
width: `${100/STATS_SCALE}%`,
|
width: `${100/STATS_SCALE}%`,
|
||||||
height: `${100/STATS_SCALE}%`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MiniStatCards
|
<MiniStatCards
|
||||||
@@ -99,6 +98,8 @@ const SmallLayout = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* You can easily add more grid items here */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -347,8 +347,8 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col space-y-2 items-center w-[250px] transition-opacity duration-300 ${mounted ? 'opacity-100' : 'opacity-0'}`}>
|
<div className="flex flex-col space-y-2 items-center w-full transition-opacity duration-300 ${mounted ? 'opacity-100' : 'opacity-0'}">
|
||||||
{/* Time Display */}
|
{/* Time Display */}
|
||||||
<Card className="bg-gradient-to-br from-slate-900 via-sky-800 to-cyan-800 dark:bg-slate-800 px-1 py-2 w-full hover:scale-[1.02] transition-transform duration-300">
|
<Card className="bg-gradient-to-br from-slate-900 via-sky-800 to-cyan-800 dark:bg-slate-800 px-1 py-2 w-full hover:scale-[1.02] transition-transform duration-300">
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<div className="flex justify-center items-baseline">
|
<div className="flex justify-center items-baseline">
|
||||||
@@ -378,15 +378,15 @@ return (
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{weather?.main && (
|
{weather?.main && (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Card className={cn(
|
<Card className={cn(
|
||||||
getWeatherBackground(
|
getWeatherBackground(
|
||||||
weather.weather[0]?.id,
|
weather.weather[0]?.id,
|
||||||
datetime.getHours() >= 18 || datetime.getHours() < 6
|
datetime.getHours() >= 18 || datetime.getHours() < 6
|
||||||
),
|
),
|
||||||
"flex items-center justify-center aspect-square cursor-pointer hover:brightness-110 transition-all relative"
|
"flex items-center justify-center aspect-square cursor-pointer hover:brightness-110 transition-all relative"
|
||||||
)}>
|
)}>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
{getWeatherIcon(weather.weather[0]?.id, datetime)}
|
{getWeatherIcon(weather.weather[0]?.id, datetime)}
|
||||||
@@ -406,30 +406,30 @@ return (
|
|||||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-[450px]"
|
className="w-[450px]"
|
||||||
align="start"
|
align="start"
|
||||||
side="right"
|
side="right"
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
style={{
|
style={{
|
||||||
transform: `scale(${scaleFactor})`,
|
transform: `scale(${scaleFactor})`,
|
||||||
transformOrigin: 'left top'
|
transformOrigin: 'left top'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{weather.alerts && (
|
{weather.alerts && (
|
||||||
<Alert variant="warning" className="mb-3">
|
<Alert variant="warning" className="mb-3">
|
||||||
<AlertTriangle className="h-3 w-3" />
|
<AlertTriangle className="h-3 w-3" />
|
||||||
<AlertDescription className="text-xs">
|
<AlertDescription className="text-xs">
|
||||||
{weather.alerts[0].event}
|
{weather.alerts[0].event}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
)}
|
||||||
|
<WeatherDetails />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
)}
|
)}
|
||||||
<WeatherDetails />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Calendar Display */}
|
{/* Calendar Display */}
|
||||||
@@ -442,7 +442,7 @@ return (
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DateTimeWeatherDisplay;
|
export default DateTimeWeatherDisplay;
|
||||||
@@ -29,9 +29,18 @@ import {
|
|||||||
Package,
|
Package,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CircleDollarSign,
|
CircleDollarSign,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
// Import the detail view components and utilities from StatCards
|
// Import the detail view components and utilities from StatCards
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +55,92 @@ import {
|
|||||||
SkeletonCard,
|
SkeletonCard,
|
||||||
} from "./StatCards";
|
} from "./StatCards";
|
||||||
|
|
||||||
|
// Mini skeleton components
|
||||||
|
const MiniSkeletonChart = ({ type = "line" }) => (
|
||||||
|
<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">
|
||||||
|
{/* Grid lines */}
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="absolute w-full h-px bg-muted"
|
||||||
|
style={{ top: `${(i + 1) * 20}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{/* Y-axis labels */}
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-6 flex flex-col justify-between py-2">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-2 w-4 bg-muted rounded-sm" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* X-axis labels */}
|
||||||
|
<div className="absolute left-6 right-2 bottom-0 flex justify-between">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-2 w-6 bg-muted rounded-sm" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{type === "bar" ? (
|
||||||
|
<div className="absolute inset-x-6 bottom-4 top-2 flex items-end justify-between gap-1">
|
||||||
|
{[...Array(24)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-1 bg-muted rounded-sm"
|
||||||
|
style={{ height: `${Math.random() * 80 + 10}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-x-6 bottom-4 top-2">
|
||||||
|
<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 MiniSkeletonTable = ({ rows = 8 }) => (
|
||||||
|
<div className="rounded-lg border bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="dark:border-gray-800">
|
||||||
|
<TableHead>
|
||||||
|
<Skeleton className="h-3 w-24 bg-muted rounded-sm" />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
<Skeleton className="h-3 w-16 ml-auto bg-muted rounded-sm" />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
<Skeleton className="h-3 w-16 ml-auto bg-muted rounded-sm" />
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{[...Array(rows)].map((_, i) => (
|
||||||
|
<TableRow key={i} className="dark:border-gray-800">
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-3 w-32 bg-muted rounded-sm" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Skeleton className="h-3 w-12 ml-auto bg-muted rounded-sm" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Skeleton className="h-3 w-12 ml-auto bg-muted rounded-sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const MiniStatCards = ({
|
const MiniStatCards = ({
|
||||||
timeRange: initialTimeRange = "today",
|
timeRange: initialTimeRange = "today",
|
||||||
startDate,
|
startDate,
|
||||||
@@ -195,6 +290,44 @@ const MiniStatCards = ({
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [timeRange]);
|
}, [timeRange]);
|
||||||
|
|
||||||
|
// Add function to fetch detail data
|
||||||
|
const fetchDetailData = useCallback(async (metric) => {
|
||||||
|
if (detailData[metric]) return;
|
||||||
|
|
||||||
|
setDetailDataLoading((prev) => ({ ...prev, [metric]: true }));
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/klaviyo/events/stats/details", {
|
||||||
|
params: {
|
||||||
|
timeRange: "last30days",
|
||||||
|
metric,
|
||||||
|
daily: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setDetailData((prev) => ({ ...prev, [metric]: response.data.stats }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching detail data for ${metric}:`, error);
|
||||||
|
} finally {
|
||||||
|
setDetailDataLoading((prev) => ({ ...prev, [metric]: false }));
|
||||||
|
}
|
||||||
|
}, [detailData]);
|
||||||
|
|
||||||
|
// Add effect to load detail data when metric is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedMetric) {
|
||||||
|
fetchDetailData(selectedMetric);
|
||||||
|
}
|
||||||
|
}, [selectedMetric, fetchDetailData]);
|
||||||
|
|
||||||
|
// Add preload effect
|
||||||
|
useEffect(() => {
|
||||||
|
// Preload all detail data when component mounts
|
||||||
|
const metrics = ["revenue", "orders", "average_order", "shipping"];
|
||||||
|
metrics.forEach(metric => {
|
||||||
|
fetchDetailData(metric);
|
||||||
|
});
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (loading && !stats) {
|
if (loading && !stats) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
@@ -227,8 +360,7 @@ const MiniStatCards = ({
|
|||||||
const aovTrend = calculateAOVTrend();
|
const aovTrend = calculateAOVTrend();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<>
|
||||||
<CardHeader className="p-4">
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
@@ -255,8 +387,7 @@ const MiniStatCards = ({
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4 pt-0">
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Total Revenue"
|
title="Total Revenue"
|
||||||
@@ -322,30 +453,62 @@ const MiniStatCards = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DetailDialog
|
<Dialog open={!!selectedMetric} onOpenChange={() => setSelectedMetric(null)}>
|
||||||
open={!!selectedMetric}
|
<DialogContent className="w-[80vw] h-[80vh] max-w-none p-0">
|
||||||
onOpenChange={() => setSelectedMetric(null)}
|
<div className="transform scale-[2] origin-top-left h-[40vh] w-[40vw]">
|
||||||
title={
|
<div className="h-full w-full p-6">
|
||||||
selectedMetric
|
<DialogHeader>
|
||||||
? `${selectedMetric
|
<DialogTitle>
|
||||||
.split("_")
|
{selectedMetric
|
||||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
? `${selectedMetric
|
||||||
.join(" ")} Details`
|
.split("_")
|
||||||
: ""
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
}
|
.join(" ")} Details`
|
||||||
>
|
: ""}
|
||||||
{selectedMetric === "revenue" && <RevenueDetails data={detailData.revenue || []} />}
|
</DialogTitle>
|
||||||
{selectedMetric === "orders" && <OrdersDetails data={detailData.orders || []} />}
|
</DialogHeader>
|
||||||
{selectedMetric === "average_order" && (
|
<div className="mt-4 h-[calc(40vh-4rem)] overflow-auto">
|
||||||
<AverageOrderDetails
|
{detailDataLoading[selectedMetric] ? (
|
||||||
data={detailData.average_order || []}
|
<div className="space-y-4 h-full">
|
||||||
orderCount={stats.orderCount}
|
{selectedMetric === "shipping" ? (
|
||||||
/>
|
<MiniSkeletonTable rows={8} />
|
||||||
)}
|
) : (
|
||||||
{selectedMetric === "shipping" && <ShippingDetails data={[stats]} timeRange={timeRange} />}
|
<>
|
||||||
</DetailDialog>
|
<MiniSkeletonChart type={selectedMetric === "orders" ? "bar" : "line"} />
|
||||||
</CardContent>
|
{selectedMetric === "orders" && (
|
||||||
</Card>
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Hourly Distribution</h3>
|
||||||
|
<MiniSkeletonChart type="bar" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full">
|
||||||
|
{selectedMetric === "revenue" && (
|
||||||
|
<RevenueDetails data={detailData.revenue || []} />
|
||||||
|
)}
|
||||||
|
{selectedMetric === "orders" && (
|
||||||
|
<OrdersDetails data={detailData.orders || []} />
|
||||||
|
)}
|
||||||
|
{selectedMetric === "average_order" && (
|
||||||
|
<AverageOrderDetails
|
||||||
|
data={detailData.average_order || []}
|
||||||
|
orderCount={stats.orderCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedMetric === "shipping" && (
|
||||||
|
<ShippingDetails data={[stats]} timeRange={timeRange} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2149,6 +2149,8 @@ export {
|
|||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatPercentage,
|
formatPercentage,
|
||||||
SkeletonCard,
|
SkeletonCard,
|
||||||
|
SkeletonChart,
|
||||||
|
SkeletonTable,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatCards;
|
export default StatCards;
|
||||||
|
|||||||
Reference in New Issue
Block a user