UNPKG

@kodeme-io/next-core-analytics

Version:

Analytics, charts, dashboards, and reporting for Next.js applications

1,302 lines (1,291 loc) 48.1 kB
// 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 };