@kodeme-io/next-core-analytics
Version:
Analytics, charts, dashboards, and reporting for Next.js applications
1,302 lines (1,291 loc) • 48.1 kB
JavaScript
// src/components/kpi-card.tsx
import { memo, useMemo } from "react";
// src/utils/formatters.ts
var DEFAULT_LOCALE = "en-US";
var DEFAULT_CURRENCY = "USD";
function formatValue(value, options = {}) {
if (typeof value === "string") return value;
const {
format,
locale = DEFAULT_LOCALE,
currency = DEFAULT_CURRENCY,
prefix,
suffix,
numberFormatOptions
} = options;
let formatted = value.toString();
switch (format) {
case "currency": {
const currencyOptions = {
style: "currency",
currency,
minimumFractionDigits: 0,
maximumFractionDigits: 2,
...numberFormatOptions
};
try {
formatted = new Intl.NumberFormat(locale, currencyOptions).format(value);
} catch (error) {
formatted = new Intl.NumberFormat(DEFAULT_LOCALE, currencyOptions).format(value);
}
break;
}
case "percentage": {
const percentageOptions = {
style: "percent",
minimumFractionDigits: 1,
maximumFractionDigits: 2,
...numberFormatOptions
};
const percentageValue = value > 1 ? value / 100 : value;
try {
formatted = new Intl.NumberFormat(locale, percentageOptions).format(percentageValue);
} catch (error) {
formatted = new Intl.NumberFormat(DEFAULT_LOCALE, percentageOptions).format(percentageValue);
}
break;
}
case "number": {
const numberOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
...numberFormatOptions
};
try {
formatted = new Intl.NumberFormat(locale, numberOptions).format(value);
} catch (error) {
formatted = new Intl.NumberFormat(DEFAULT_LOCALE, numberOptions).format(value);
}
break;
}
case "duration": {
formatted = formatDuration(value);
break;
}
default:
try {
formatted = new Intl.NumberFormat(locale).format(value);
} catch (error) {
formatted = new Intl.NumberFormat(DEFAULT_LOCALE).format(value);
}
}
if (prefix) formatted = `${prefix}${formatted}`;
if (suffix) formatted = `${formatted}${suffix}`;
return formatted;
}
function formatDuration(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor(seconds % 86400 / 3600);
const minutes = Math.floor(seconds % 3600 / 60);
const remainingSeconds = Math.floor(seconds % 60);
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`);
return parts.join(" ");
}
function formatTrendValue(value, format = "percentage") {
const sign = value > 0 ? "+" : value < 0 ? "-" : "";
const absValue = Math.abs(value);
if (format === "percentage") {
return `${sign}${absValue.toFixed(1)}%`;
}
return `${sign}${absValue}`;
}
function createFormatter(options) {
return (value, additionalOptions = {}) => {
return formatValue(value, { ...options, ...additionalOptions });
};
}
var currencyFormatters = {
USD: createFormatter({ format: "currency", currency: "USD", locale: "en-US" }),
EUR: createFormatter({ format: "currency", currency: "EUR", locale: "de-DE" }),
GBP: createFormatter({ format: "currency", currency: "GBP", locale: "en-GB" }),
JPY: createFormatter({ format: "currency", currency: "JPY", locale: "ja-JP" }),
CNY: createFormatter({ format: "currency", currency: "CNY", locale: "zh-CN" }),
IDR: createFormatter({ format: "currency", currency: "IDR", locale: "id-ID" })
};
var numberFormatters = {
default: createFormatter({ format: "number", locale: "en-US" }),
european: createFormatter({ format: "number", locale: "de-DE" }),
asian: createFormatter({ format: "number", locale: "zh-CN" })
};
var percentageFormatters = {
default: createFormatter({ format: "percentage", locale: "en-US" }),
european: createFormatter({ format: "percentage", locale: "de-DE" })
};
// src/utils/validation.ts
import { z } from "zod";
var ChartDataPointSchema = z.object({
name: z.string(),
value: z.number()
});
var MultiSeriesDataPointSchema = z.record(z.string(), z.union([z.string(), z.number()]));
var KPIMetricSchema = z.object({
id: z.string().min(1, "KPI ID is required"),
label: z.string().min(1, "KPI label is required"),
value: z.union([z.string(), z.number()]),
previousValue: z.number().optional(),
format: z.enum(["number", "currency", "percentage", "duration"]).optional(),
trend: z.enum(["up", "down", "neutral"]).optional(),
trendValue: z.number().optional(),
color: z.string().optional(),
prefix: z.string().optional(),
suffix: z.string().optional(),
locale: z.string().optional(),
currency: z.string().optional(),
numberFormatOptions: z.record(z.any()).optional(),
icon: z.any().optional()
});
var ChartConfigSchema = z.object({
type: z.enum(["line", "bar", "area", "pie", "donut", "scatter", "radar"]),
data: z.array(z.any()),
xKey: z.string().optional(),
yKey: z.union([z.string(), z.array(z.string())]).optional(),
colors: z.array(z.string()).optional(),
legend: z.boolean().optional(),
grid: z.boolean().optional(),
tooltip: z.boolean().optional(),
title: z.string().optional(),
subtitle: z.string().optional()
});
var DashboardWidgetSchema = z.object({
id: z.string().min(1, "Widget ID is required"),
type: z.enum(["chart", "kpi", "table", "custom"]),
title: z.string().min(1, "Widget title is required"),
description: z.string().optional(),
config: z.any(),
span: z.object({
cols: z.number().min(1).max(12).optional(),
rows: z.number().min(1).max(6).optional()
}).optional()
});
var DashboardLayoutSchema = z.object({
id: z.string().min(1, "Dashboard ID is required"),
name: z.string().min(1, "Dashboard name is required"),
widgets: z.array(DashboardWidgetSchema),
refreshInterval: z.number().positive().optional(),
filters: z.array(z.object({
id: z.string(),
type: z.enum(["date", "select", "multiselect", "range"]),
label: z.string(),
value: z.any(),
options: z.array(z.object({
label: z.string(),
value: z.any()
})).optional()
})).optional()
});
var ExportConfigSchema = z.object({
format: z.enum(["pdf", "excel", "csv", "json"]),
filename: z.string().optional(),
data: z.array(z.any()),
columns: z.array(z.string()).optional(),
title: z.string().optional(),
includeCharts: z.boolean().optional()
});
var AnalyticsRequestSchema = z.object({
endpoint: z.string().url("Invalid URL provided for endpoint"),
params: z.record(z.any()).optional(),
method: z.enum(["GET", "POST"]).optional(),
headers: z.record(z.string()).optional(),
cache: z.enum(["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]).optional()
});
function validateChartData(data) {
return data.map((item) => ChartDataPointSchema.parse(item));
}
function validateKPI(metric) {
return KPIMetricSchema.parse(metric);
}
function validateKPIs(metrics) {
return metrics.map((metric) => KPIMetricSchema.parse(metric));
}
function validateChartConfig(config) {
return ChartConfigSchema.parse(config);
}
function validateDashboardLayout(layout) {
return DashboardLayoutSchema.parse(layout);
}
function validateExportConfig(config) {
return ExportConfigSchema.parse(config);
}
function validateAnalyticsRequest(options) {
return AnalyticsRequestSchema.parse(options);
}
function safeValidate(schema, data) {
const result = schema.safeParse(data);
if (result.success) {
return {
success: true,
data: result.data
};
} else {
return {
success: false,
errors: result.error
};
}
}
function getValidationErrors(error) {
return error.errors.map((err) => `${err.path.join(".")}: ${err.message}`);
}
function createValidationMonitor() {
const validationStats = {
totalValidations: 0,
failedValidations: 0,
totalValidationTime: 0
};
return {
validate: (schema, data) => {
const startTime = performance.now();
validationStats.totalValidations++;
try {
const result = schema.parse(data);
const endTime = performance.now();
validationStats.totalValidationTime += endTime - startTime;
return result;
} catch (error) {
validationStats.failedValidations++;
const endTime = performance.now();
validationStats.totalValidationTime += endTime - startTime;
throw error;
}
},
getStats: () => ({
...validationStats,
averageValidationTime: validationStats.totalValidations > 0 ? validationStats.totalValidationTime / validationStats.totalValidations : 0,
successRate: validationStats.totalValidations > 0 ? (validationStats.totalValidations - validationStats.failedValidations) / validationStats.totalValidations * 100 : 100
}),
reset: () => {
validationStats.totalValidations = 0;
validationStats.failedValidations = 0;
validationStats.totalValidationTime = 0;
}
};
}
var validationMonitor = createValidationMonitor();
// src/components/kpi-card.tsx
import { jsx, jsxs } from "react/jsx-runtime";
var getTrendIcon = (trend) => {
if (!trend || trend === "neutral") return null;
return trend === "up" ? /* @__PURE__ */ jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M14.707 12.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l2.293-2.293a1 1 0 011.414 0z", clipRule: "evenodd" }) });
};
var getTrendColor = (trend) => {
switch (trend) {
case "up":
return "text-green-600";
case "down":
return "text-red-600";
default:
return "text-gray-500";
}
};
var renderIcon = (icon) => {
if (!icon) return null;
if (typeof icon === "function") {
const IconComponent = icon;
return /* @__PURE__ */ jsx(IconComponent, { className: "w-6 h-6" });
}
return icon;
};
var KPICard = memo(({
metric,
variant = "default",
className = ""
}) => {
const validation = useMemo(() => safeValidate(KPIMetricSchema, metric), [metric]);
if (!validation.success) {
console.error("Invalid KPI metric:", validation.errors);
return /* @__PURE__ */ jsx("div", { className: `bg-red-50 border border-red-200 rounded-lg p-4 ${className}`, children: /* @__PURE__ */ jsx("div", { className: "text-red-600", children: "Invalid KPI metric configuration" }) });
}
const validatedMetric = validation.data;
const trendColor = useMemo(() => getTrendColor(validatedMetric.trend), [validatedMetric.trend]);
const trendIcon = useMemo(() => getTrendIcon(validatedMetric.trend), [validatedMetric.trend]);
const iconElement = useMemo(() => renderIcon(validatedMetric.icon), [validatedMetric.icon]);
const formattedValue = useMemo(() => formatValue(validatedMetric.value, {
format: validatedMetric.format,
locale: validatedMetric.locale,
currency: validatedMetric.currency,
prefix: validatedMetric.prefix,
suffix: validatedMetric.suffix,
numberFormatOptions: validatedMetric.numberFormatOptions
}), [validatedMetric.value, validatedMetric.format, validatedMetric.locale, validatedMetric.currency, validatedMetric.prefix, validatedMetric.suffix, validatedMetric.numberFormatOptions]);
const formattedPreviousValue = useMemo(
() => validatedMetric.previousValue !== void 0 ? formatValue(validatedMetric.previousValue, {
format: validatedMetric.format,
locale: validatedMetric.locale,
currency: validatedMetric.currency,
prefix: validatedMetric.prefix,
suffix: validatedMetric.suffix,
numberFormatOptions: validatedMetric.numberFormatOptions
}) : null,
[validatedMetric.previousValue, validatedMetric.format, validatedMetric.locale, validatedMetric.currency, validatedMetric.prefix, validatedMetric.suffix, validatedMetric.numberFormatOptions]
);
const formattedTrendValue = useMemo(
() => validatedMetric.trendValue !== void 0 ? formatTrendValue(validatedMetric.trendValue) : null,
[validatedMetric.trendValue]
);
if (variant === "compact") {
return /* @__PURE__ */ jsxs("div", { className: `bg-white rounded-lg border p-4 ${className}`, children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600", children: validatedMetric.label }),
/* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold mt-1", children: formatValue(validatedMetric.value, {
format: validatedMetric.format,
locale: validatedMetric.locale,
currency: validatedMetric.currency,
prefix: validatedMetric.prefix,
suffix: validatedMetric.suffix,
numberFormatOptions: validatedMetric.numberFormatOptions
}) })
] }),
iconElement && /* @__PURE__ */ jsx("div", { className: `ml-4 ${validatedMetric.color || "text-blue-500"}`, children: iconElement })
] }),
validatedMetric.trendValue !== void 0 && /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1 mt-2 text-sm ${trendColor}`, children: [
trendIcon,
/* @__PURE__ */ jsx("span", { children: formatTrendValue(validatedMetric.trendValue) })
] })
] });
}
if (variant === "detailed") {
return /* @__PURE__ */ jsxs("div", { className: `bg-white rounded-lg border p-6 ${className}`, children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between mb-4", children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-600", children: metric.label }),
iconElement && /* @__PURE__ */ jsx("div", { className: `mt-2 ${metric.color || "text-blue-500"}`, children: iconElement })
] }),
metric.trendValue !== void 0 && /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1 text-sm font-medium ${trendColor}`, children: [
trendIcon,
/* @__PURE__ */ jsx("span", { children: formatTrendValue(metric.trendValue) })
] })
] }),
/* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: formatValue(metric.value, {
format: metric.format,
locale: metric.locale,
currency: metric.currency,
prefix: metric.prefix,
suffix: metric.suffix,
numberFormatOptions: metric.numberFormatOptions
}) }),
metric.previousValue !== void 0 && /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500 mt-2", children: [
"Previous: ",
formatValue(metric.previousValue, {
format: metric.format,
locale: metric.locale,
currency: metric.currency,
prefix: metric.prefix,
suffix: metric.suffix,
numberFormatOptions: metric.numberFormatOptions
})
] })
] });
}
return /* @__PURE__ */ jsxs(
"div",
{
className: `bg-white rounded-lg border p-5 ${className}`,
role: "region",
"aria-label": `${metric.label}: ${formatValue(metric.value, {
format: metric.format,
locale: metric.locale,
currency: metric.currency,
prefix: metric.prefix,
suffix: metric.suffix,
numberFormatOptions: metric.numberFormatOptions
})}`,
children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-600", children: metric.label }),
iconElement && /* @__PURE__ */ jsx("div", { className: `${metric.color || "text-blue-500"}`, children: iconElement })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between", children: [
/* @__PURE__ */ jsx("p", { className: "text-3xl font-semibold", children: formatValue(metric.value, {
format: metric.format,
locale: metric.locale,
currency: metric.currency,
prefix: metric.prefix,
suffix: metric.suffix,
numberFormatOptions: metric.numberFormatOptions
}) }),
metric.trendValue !== void 0 && /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1 text-sm font-medium ${trendColor}`, children: [
trendIcon,
/* @__PURE__ */ jsx("span", { children: formatTrendValue(metric.trendValue) })
] })
] })
]
}
);
});
KPICard.displayName = "KPICard";
// src/components/chart-container.tsx
import { memo as memo2, useMemo as useMemo2 } from "react";
import {
LineChart,
BarChart,
PieChart,
AreaChart,
RadarChart,
ScatterChart,
Line,
Bar,
Pie,
Area,
Radar,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
PolarGrid,
PolarAngleAxis,
PolarRadiusAxis
} from "recharts";
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var ChartContainer = memo2(({
config,
loading = false,
error,
className = ""
}) => {
const memoizedData = useMemo2(() => config.data, [config.data]);
const commonProps = useMemo2(() => ({
data: memoizedData,
margin: { top: 5, right: 30, left: 20, bottom: 5 }
}), [memoizedData]);
const renderMultipleSeries = useMemo2(() => (Component2) => {
if (Array.isArray(config.yKey)) {
return config.yKey.map((key, index) => /* @__PURE__ */ jsx2(
Component2,
{
type: "monotone",
dataKey: key,
stroke: config.colors?.[index] || `hsl(${index * 60}, 70%, 50%)`,
fill: config.colors?.[index] || `hsl(${index * 60}, 70%, 50%)`,
fillOpacity: Component2 === Area ? 0.3 : 1,
strokeWidth: 2
},
key
));
} else {
return /* @__PURE__ */ jsx2(
Component2,
{
type: "monotone",
dataKey: config.yKey,
stroke: config.colors?.[0] || "#3b82f6",
fill: config.colors?.[0] || "#3b82f6",
fillOpacity: Component2 === Area ? 0.3 : 1,
strokeWidth: 2
}
);
}
}, [config.yKey, config.colors]);
if (loading) {
return /* @__PURE__ */ jsx2("div", { className: `flex items-center justify-center p-8 ${className}`, children: /* @__PURE__ */ jsx2("div", { className: "text-gray-500", children: "Loading..." }) });
}
if (error) {
return /* @__PURE__ */ jsx2("div", { className: `p-4 border border-red-200 rounded-lg ${className}`, children: /* @__PURE__ */ jsx2("div", { className: "text-red-600", children: error }) });
}
const renderChart = () => {
const commonProps2 = {
data: config.data,
margin: { top: 5, right: 30, left: 20, bottom: 5 }
};
const renderMultipleSeries2 = (Component2, baseKey) => {
if (Array.isArray(config.yKey)) {
return config.yKey.map((key, index) => /* @__PURE__ */ jsx2(
Component2,
{
type: "monotone",
dataKey: key,
stroke: config.colors?.[index] || `hsl(${index * 60}, 70%, 50%)`,
fill: config.colors?.[index] || `hsl(${index * 60}, 70%, 50%)`,
fillOpacity: Component2 === Area ? 0.3 : 1,
strokeWidth: 2
},
key
));
} else {
return /* @__PURE__ */ jsx2(
Component2,
{
type: "monotone",
dataKey: config.yKey,
stroke: config.colors?.[0] || "#3b82f6",
fill: config.colors?.[0] || "#3b82f6",
fillOpacity: Component2 === Area ? 0.3 : 1,
strokeWidth: 2
}
);
}
};
switch (config.type) {
case "line":
return /* @__PURE__ */ jsxs2(LineChart, { ...commonProps2, children: [
/* @__PURE__ */ jsx2(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx2(XAxis, { dataKey: config.xKey }),
/* @__PURE__ */ jsx2(YAxis, {}),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {}),
renderMultipleSeries2(Line, "line")
] });
case "bar":
return /* @__PURE__ */ jsxs2(BarChart, { ...commonProps2, children: [
/* @__PURE__ */ jsx2(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx2(XAxis, { dataKey: config.xKey }),
/* @__PURE__ */ jsx2(YAxis, {}),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {}),
Array.isArray(config.yKey) ? config.yKey.map((key, index) => /* @__PURE__ */ jsx2(
Bar,
{
dataKey: key,
fill: config.colors?.[index] || `hsl(${index * 60}, 70%, 50%)`
},
key
)) : /* @__PURE__ */ jsx2(Bar, { dataKey: config.yKey, fill: config.colors?.[0] || "#3b82f6" })
] });
case "pie":
return /* @__PURE__ */ jsxs2(PieChart, { children: [
/* @__PURE__ */ jsx2(
Pie,
{
data: config.data,
dataKey: config.yKey,
nameKey: config.xKey,
cx: "50%",
cy: "50%",
outerRadius: 80,
fill: config.colors?.[0] || "#3b82f6"
}
),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {})
] });
case "donut":
return /* @__PURE__ */ jsxs2(PieChart, { children: [
/* @__PURE__ */ jsx2(
Pie,
{
data: config.data,
dataKey: config.yKey,
nameKey: config.xKey,
cx: "50%",
cy: "50%",
outerRadius: 80,
innerRadius: 40,
fill: config.colors?.[0] || "#3b82f6"
}
),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {})
] });
case "area":
return /* @__PURE__ */ jsxs2(AreaChart, { ...commonProps2, children: [
/* @__PURE__ */ jsx2(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx2(XAxis, { dataKey: config.xKey }),
/* @__PURE__ */ jsx2(YAxis, {}),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {}),
renderMultipleSeries2(Area, "area")
] });
case "scatter":
return /* @__PURE__ */ jsxs2(ScatterChart, { ...commonProps2, children: [
/* @__PURE__ */ jsx2(CartesianGrid, { strokeDasharray: "3 3" }),
/* @__PURE__ */ jsx2(XAxis, { dataKey: config.xKey }),
/* @__PURE__ */ jsx2(YAxis, { dataKey: config.yKey }),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {}),
/* @__PURE__ */ jsx2(
Scatter,
{
name: "Data Points",
data: config.data,
fill: config.colors?.[0] || "#3b82f6"
}
)
] });
case "radar":
return /* @__PURE__ */ jsxs2(RadarChart, { ...commonProps2, children: [
/* @__PURE__ */ jsx2(PolarGrid, {}),
/* @__PURE__ */ jsx2(PolarAngleAxis, { dataKey: config.xKey }),
/* @__PURE__ */ jsx2(PolarRadiusAxis, {}),
/* @__PURE__ */ jsx2(
Radar,
{
name: "Values",
dataKey: config.yKey,
stroke: config.colors?.[0] || "#3b82f6",
fill: config.colors?.[0] || "#3b82f6",
fillOpacity: 0.3
}
),
/* @__PURE__ */ jsx2(Tooltip, {}),
/* @__PURE__ */ jsx2(Legend, {})
] });
default:
return /* @__PURE__ */ jsxs2("div", { children: [
"Unsupported chart type: ",
config.type
] });
}
};
return /* @__PURE__ */ jsxs2(
"div",
{
className: `bg-white p-4 rounded-lg border ${className}`,
role: "img",
"aria-label": config.title || `Chart showing ${config.type} data`,
children: [
config.title && /* @__PURE__ */ jsxs2("div", { className: "mb-4", children: [
/* @__PURE__ */ jsx2("h3", { className: "text-lg font-semibold", children: config.title }),
config.subtitle && /* @__PURE__ */ jsx2("p", { className: "text-sm text-gray-600", children: config.subtitle })
] }),
/* @__PURE__ */ jsx2(ResponsiveContainer, { width: "100%", height: 300, children: renderChart() })
]
}
);
});
ChartContainer.displayName = "ChartContainer";
// src/components/dashboard.tsx
import { useState, useEffect } from "react";
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
var Dashboard = ({
layout,
onRefresh,
className = ""
}) => {
const [filters, setFilters] = useState(layout.filters || []);
useEffect(() => {
if (layout.refreshInterval && onRefresh) {
const interval = setInterval(onRefresh, layout.refreshInterval * 1e3);
return () => clearInterval(interval);
}
}, [layout.refreshInterval, onRefresh]);
const handleFilterChange = (filterId, value) => {
setFilters(
(prev) => prev.map(
(filter) => filter.id === filterId ? { ...filter, value } : filter
)
);
};
return /* @__PURE__ */ jsxs3("div", { className: `p-6 ${className}`, children: [
/* @__PURE__ */ jsx3("div", { className: "mb-6", children: /* @__PURE__ */ jsx3("h1", { className: "text-2xl font-bold", children: layout.name }) }),
filters.length > 0 && /* @__PURE__ */ jsx3("div", { className: "mb-6 flex flex-wrap gap-4", children: filters.map((filter) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
/* @__PURE__ */ jsx3("label", { className: "text-sm font-medium mb-1", children: filter.label }),
filter.type === "date" ? /* @__PURE__ */ jsx3(
"input",
{
type: "date",
value: filter.value || "",
onChange: (e) => handleFilterChange(filter.id, e.target.value),
className: "px-3 py-2 border rounded-md",
"aria-label": filter.label
}
) : /* @__PURE__ */ jsx3(
"input",
{
type: "text",
value: filter.value || "",
onChange: (e) => handleFilterChange(filter.id, e.target.value),
className: "px-3 py-2 border rounded-md",
"aria-label": filter.label
}
)
] }, filter.id)) }),
/* @__PURE__ */ jsx3("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6", children: layout.widgets.map((widget) => /* @__PURE__ */ jsxs3(
"div",
{
className: `
${widget.span?.cols ? `col-span-${widget.span.cols}` : ""}
${widget.span?.rows ? `row-span-${widget.span.rows}` : ""}
`,
children: [
widget.type === "kpi" && /* @__PURE__ */ jsxs3("div", { className: "bg-white p-4 rounded-lg border", children: [
/* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold mb-3", children: widget.title }),
/* @__PURE__ */ jsx3("div", { className: "space-y-4", children: widget.config.metrics?.map((metric) => /* @__PURE__ */ jsx3(KPICard, { metric }, metric.id)) })
] }),
widget.type === "chart" && /* @__PURE__ */ jsxs3("div", { className: "bg-white p-4 rounded-lg border", children: [
/* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold mb-3", children: widget.title }),
/* @__PURE__ */ jsx3(ChartContainer, { config: widget.config })
] }),
widget.type !== "kpi" && widget.type !== "chart" && /* @__PURE__ */ jsxs3("div", { className: "bg-white p-4 rounded-lg border", children: [
/* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold", children: widget.title }),
/* @__PURE__ */ jsxs3("div", { className: "text-gray-500", children: [
"Unknown widget type: ",
widget.type
] })
] })
]
},
widget.id
)) })
] });
};
// src/components/export-button.tsx
import { useState as useState2 } from "react";
import jsPDF from "jspdf";
import * as XLSX from "xlsx";
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
var ExportButton = ({
config,
className = ""
}) => {
const [isExporting, setIsExporting] = useState2(false);
const [error, setError] = useState2(null);
const handleExport = async () => {
setIsExporting(true);
setError(null);
try {
switch (config.format) {
case "pdf":
await exportToPDF(config);
break;
case "excel":
await exportToExcel(config);
break;
case "csv":
await exportToCSV(config);
break;
case "json":
await exportToJSON(config);
break;
default:
throw new Error(`Unsupported export format: ${config.format}`);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Export failed");
} finally {
setIsExporting(false);
}
};
return /* @__PURE__ */ jsxs4("div", { className: `inline-block ${className}`, children: [
/* @__PURE__ */ jsx4(
"button",
{
onClick: handleExport,
disabled: isExporting,
className: "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50",
role: "button",
"aria-label": `Export to ${config.format}`,
children: isExporting ? "Exporting..." : `Export to ${config.format.charAt(0).toUpperCase() + config.format.slice(1)}`
}
),
error && /* @__PURE__ */ jsx4("div", { className: "mt-2 text-sm text-red-600", role: "alert", children: error })
] });
};
async function exportToPDF(config) {
const doc = new jsPDF();
if (config.title) {
doc.setFontSize(16);
doc.text(config.title, 10, 20);
}
if (config.data && config.data.length > 0) {
const headers = config.columns || Object.keys(config.data[0]);
const rows = config.data.map(
(item) => headers.map((header) => String(item[header] || ""))
);
doc.autoTable({
head: [headers],
body: rows,
startY: config.title ? 30 : 20,
theme: "striped",
styles: { fontSize: 10 }
});
}
doc.save(`${config.filename || "export"}.pdf`);
}
async function exportToExcel(config) {
const ws = XLSX.utils.json_to_sheet(config.data);
const wb = XLSX.utils.book_new();
if (config.title) {
XLSX.utils.book_append_sheet(wb, ws, config.title);
} else {
XLSX.utils.book_append_sheet(wb, ws, "Data");
}
XLSX.writeFile(wb, `${config.filename || "export"}.xlsx`);
}
async function exportToJSON(config) {
const jsonString = JSON.stringify(config.data, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${config.filename || "export"}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async function exportToCSV(config) {
const headers = config.columns || Object.keys(config.data[0] || {});
const csvContent = [
headers.join(","),
...config.data.map(
(row) => headers.map((header) => {
const value = row[header];
return typeof value === "string" && value.includes(",") ? `"${value}"` : value;
}).join(",")
)
].join("\n");
const blob = new Blob([csvContent], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${config.filename}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// src/components/error-boundary.tsx
import React5, { Component } from "react";
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
var AnalyticsErrorBoundary = class extends Component {
constructor(props) {
super(props);
this.handleReset = () => {
this.setState({ hasError: false, error: void 0, errorInfo: void 0 });
};
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return {
hasError: true,
error
};
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
if (process.env.NODE_ENV === "development") {
console.error("Analytics Error Boundary caught an error:", error, errorInfo);
}
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return /* @__PURE__ */ jsx5("div", { className: "bg-red-50 border border-red-200 rounded-lg p-6 m-4", children: /* @__PURE__ */ jsxs5("div", { className: "flex items-start", children: [
/* @__PURE__ */ jsx5("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx5(
"svg",
{
className: "h-6 w-6 text-red-400",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor",
children: /* @__PURE__ */ jsx5(
"path",
{
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
}
)
}
) }),
/* @__PURE__ */ jsxs5("div", { className: "ml-3 flex-1", children: [
/* @__PURE__ */ jsx5("h3", { className: "text-sm font-medium text-red-800", children: "Analytics Component Error" }),
/* @__PURE__ */ jsxs5("div", { className: "mt-2 text-sm text-red-700", children: [
/* @__PURE__ */ jsx5("p", { children: "An error occurred while rendering the analytics component." }),
this.props.showDetails && this.state.error && /* @__PURE__ */ jsxs5("details", { className: "mt-2", children: [
/* @__PURE__ */ jsx5("summary", { className: "cursor-pointer font-medium", children: "Error Details" }),
/* @__PURE__ */ jsxs5("div", { className: "mt-2 p-2 bg-red-100 rounded text-xs font-mono", children: [
/* @__PURE__ */ jsx5("div", { className: "text-red-900", children: this.state.error.message }),
this.state.errorInfo && /* @__PURE__ */ jsxs5("div", { className: "mt-1 text-red-700", children: [
"Component Stack:",
/* @__PURE__ */ jsx5("pre", { className: "whitespace-pre-wrap", children: this.state.errorInfo.componentStack })
] })
] })
] })
] }),
/* @__PURE__ */ jsxs5("div", { className: "mt-4 flex space-x-3", children: [
/* @__PURE__ */ jsx5(
"button",
{
type: "button",
onClick: this.handleReset,
className: "bg-red-100 text-red-700 px-4 py-2 rounded-md text-sm font-medium hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2",
children: "Try Again"
}
),
/* @__PURE__ */ jsx5(
"button",
{
type: "button",
onClick: () => window.location.reload(),
className: "text-red-700 px-4 py-2 rounded-md text-sm font-medium hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2",
children: "Reload Page"
}
)
] })
] })
] }) });
}
return this.props.children;
}
};
function KPIErrorBoundary({ children }) {
return /* @__PURE__ */ jsx5(
AnalyticsErrorBoundary,
{
fallback: /* @__PURE__ */ jsx5("div", { className: "bg-yellow-50 border border-yellow-200 rounded-lg p-4 m-2", children: /* @__PURE__ */ jsxs5("div", { className: "flex items-center", children: [
/* @__PURE__ */ jsx5(
"svg",
{
className: "h-5 w-5 text-yellow-400 mr-2",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor",
children: /* @__PURE__ */ jsx5(
"path",
{
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
}
)
}
),
/* @__PURE__ */ jsx5("span", { className: "text-yellow-800 text-sm", children: "Unable to display KPI data" })
] }) }),
onError: (error) => {
console.warn("KPI Card Error:", error);
},
children
}
);
}
function ChartErrorBoundary({ children }) {
return /* @__PURE__ */ jsx5(
AnalyticsErrorBoundary,
{
fallback: /* @__PURE__ */ jsxs5("div", { className: "bg-red-50 border border-red-200 rounded-lg p-8 m-2 text-center", children: [
/* @__PURE__ */ jsx5(
"svg",
{
className: "h-12 w-12 text-red-400 mx-auto mb-4",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor",
children: /* @__PURE__ */ jsx5(
"path",
{
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
}
)
}
),
/* @__PURE__ */ jsx5("h3", { className: "text-lg font-medium text-red-800 mb-2", children: "Chart Display Error" }),
/* @__PURE__ */ jsx5("p", { className: "text-red-600 text-sm", children: "Unable to render chart due to an error" })
] }),
onError: (error) => {
console.warn("Chart Error:", error);
},
children
}
);
}
function DashboardErrorBoundary({ children }) {
return /* @__PURE__ */ jsx5(
AnalyticsErrorBoundary,
{
fallback: /* @__PURE__ */ jsxs5("div", { className: "bg-gray-50 border border-gray-200 rounded-lg p-8 m-4 text-center", children: [
/* @__PURE__ */ jsx5(
"svg",
{
className: "h-16 w-16 text-gray-400 mx-auto mb-4",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor",
children: /* @__PURE__ */ jsx5(
"path",
{
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
}
)
}
),
/* @__PURE__ */ jsx5("h3", { className: "text-xl font-medium text-gray-900 mb-2", children: "Dashboard Error" }),
/* @__PURE__ */ jsx5("p", { className: "text-gray-600 mb-4", children: "The dashboard encountered an error and could not be displayed" }),
/* @__PURE__ */ jsx5(
"button",
{
onClick: () => window.location.reload(),
className: "bg-blue-100 text-blue-700 px-6 py-2 rounded-md text-sm font-medium hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
children: "Reload Dashboard"
}
)
] }),
onError: (error) => {
console.error("Dashboard Error:", error);
},
showDetails: process.env.NODE_ENV === "development",
children
}
);
}
function useErrorHandler() {
const [error, setError] = React5.useState(null);
const resetError = React5.useCallback(() => {
setError(null);
}, []);
const captureError = React5.useCallback((error2) => {
console.error("Analytics Hook Error:", error2);
setError(error2);
}, []);
return {
error,
captureError,
resetError
};
}
// src/hooks/use-analytics.ts
import { useState as useState3, useEffect as useEffect2, useCallback } from "react";
var useAnalytics = (options) => {
const [data, setData] = useState3(null);
const [loading, setLoading] = useState3(true);
const [error, setError] = useState3(null);
const [subscriptions, setSubscriptions] = useState3({});
const fetchData = useCallback(async () => {
if (!options.endpoint) {
throw new Error("Endpoint is required for analytics data fetching");
}
try {
setLoading(true);
setError(null);
const { method = "POST", headers = {}, params, cache = "default" } = options;
const requestHeaders = {
"Content-Type": "application/json",
...headers
};
const requestBody = method === "POST" ? JSON.stringify({ params }) : void 0;
let url = options.endpoint;
if (method === "GET" && params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== void 0 && value !== null) {
searchParams.append(key, String(value));
}
});
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
const response = await fetch(url, {
method,
headers: requestHeaders,
body: requestBody,
cache,
// Add next: { revalidate } for Next.js App Router
...options.revalidate && typeof window === "undefined" && {
next: { revalidate: options.revalidate }
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get("content-type");
let data2;
if (contentType && contentType.includes("application/json")) {
data2 = await response.json();
} else {
data2 = await response.text();
try {
data2 = JSON.parse(data2);
} catch {
throw new Error("Invalid response format");
}
}
if (Array.isArray(data2)) {
setData(data2);
} else if (data2.data && Array.isArray(data2.data)) {
setData(data2.data);
} else if (data2.results && Array.isArray(data2.results)) {
setData(data2.results);
} else {
console.warn("Unexpected data format:", data2);
setData([]);
}
} catch (err) {
const error2 = err instanceof Error ? err : new Error("Failed to fetch data");
setError(error2);
console.error("Analytics fetch error:", error2);
} finally {
setLoading(false);
}
}, [options.endpoint, options.params, options.method, options.headers, options.cache, options.revalidate]);
const refresh = useCallback(async () => {
await fetchData();
}, [fetchData]);
const subscribe = useCallback((event, callback) => {
setSubscriptions((prev) => ({
...prev,
[event]: [...prev[event] || [], callback]
}));
if (options.realtime) {
const interval = setInterval(() => {
callback({ timestamp: Date.now(), data: "new data" });
}, 5e3);
return () => clearInterval(interval);
}
}, [options.realtime]);
const unsubscribe = useCallback((event) => {
setSubscriptions((prev) => ({
...prev,
[event]: []
}));
}, []);
useEffect2(() => {
fetchData();
if (options.refreshInterval) {
const interval = setInterval(refresh, options.refreshInterval * 1e3);
return () => clearInterval(interval);
}
}, [fetchData, refresh, options.refreshInterval]);
return {
data,
loading,
error,
refresh,
subscribe,
unsubscribe
};
};
// src/utils/data-aggregation.ts
function aggregateData(data, config) {
const grouped = data.reduce((acc, item) => {
const groupByField = config.groupBy || "id";
const key = item[groupByField];
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item[config.field]);
return acc;
}, {});
return Object.entries(grouped).map(([key, values]) => {
let aggregatedValue;
const groupByField = config.groupBy || "id";
switch (config.type) {
case "sum":
aggregatedValue = values.reduce((a, b) => a + b, 0);
break;
case "avg":
aggregatedValue = values.reduce((a, b) => a + b, 0) / values.length;
break;
case "min":
aggregatedValue = Math.min(...values);
break;
case "max":
aggregatedValue = Math.max(...values);
break;
case "count":
aggregatedValue = values.length;
break;
default:
aggregatedValue = values[0];
}
const result = {};
result[groupByField] = key;
result[config.field] = aggregatedValue;
return result;
});
}
function processTimeSeries(data, config) {
const result = data.map((item) => ({
date: item[config.dateField],
value: item[config.valueField]
}));
if (config.fillGaps && data.length > 1) {
const dates = result.map((item) => new Date(item.date));
const minDate = new Date(Math.min(...dates.map((d) => d.getTime())));
const maxDate = new Date(Math.max(...dates.map((d) => d.getTime())));
const filledResult = [];
const currentDate = new Date(minDate);
while (currentDate <= maxDate) {
const dateStr = currentDate.toISOString().split("T")[0];
const existingData = result.find((item) => item.date === dateStr);
filledResult.push({
date: dateStr,
value: existingData ? existingData.value : 0
});
currentDate.setDate(currentDate.getDate() + 1);
}
return filledResult;
}
return result;
}
function calculateComparison(options) {
const currentSum = options.data.filter((d) => new Date(d.date) >= options.currentPeriod.start && new Date(d.date) <= options.currentPeriod.end).reduce((sum, item) => sum + item[options.metric], 0);
const previousSum = options.data.filter((d) => new Date(d.date) >= options.previousPeriod.start && new Date(d.date) <= options.previousPeriod.end).reduce((sum, item) => sum + item[options.metric], 0);
const percentChange = previousSum === 0 ? Infinity : (currentSum - previousSum) / previousSum * 100;
const trend = percentChange > 0 ? "up" : percentChange < 0 ? "down" : "neutral";
return {
current: currentSum,
previous: previousSum,
percentChange,
trend
};
}
// src/index.ts
var version = "0.8.0";
export {
AnalyticsErrorBoundary,
AnalyticsRequestSchema,
ChartConfigSchema,
ChartContainer,
ChartDataPointSchema,
ChartErrorBoundary,
Dashboard,
DashboardErrorBoundary,
DashboardLayoutSchema,
DashboardWidgetSchema,
ExportButton,
ExportConfigSchema,
KPICard,
KPIErrorBoundary,
KPIMetricSchema,
MultiSeriesDataPointSchema,
aggregateData,
calculateComparison,
createFormatter,
createValidationMonitor,
currencyFormatters,
formatDuration,
formatTrendValue,
formatValue,
getValidationErrors,
numberFormatters,
percentageFormatters,
processTimeSeries,
safeValidate,
useAnalytics,
useErrorHandler,
validateAnalyticsRequest,
validateChartConfig,
validateChartData,
validateDashboardLayout,
validateExportConfig,
validateKPI,
validateKPIs,
validationMonitor,
version
};