mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-13 18:55:43 +01:00
fix: comprehensive TypeScript/build fixes and modernization
- Update tsconfig to ES2024 target and bundler moduleResolution - Add dynamic imports for chart.js and recharts (bundle optimization) - Consolidate 17 useState into useReducer in sessions page - Fix 18 .js extension imports across lib files - Add type declarations for @rapideditor/country-coder - Fix platform user types (PlatformUserRole enum) - Fix Calendar component prop types - Centralize next-auth type augmentation - Add force-dynamic to all API routes (prevent build-time prerender) - Fix Prisma JSON null handling with Prisma.DbNull - Fix various type mismatches (SessionMessage, ImportRecord, etc.) - Export ButtonProps from button component - Update next-themes import path - Replace JSX.Element with React.ReactElement - Remove obsolete debug scripts and pnpm lockfile - Downgrade eslint to v8 for next compatibility
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Chart, { type BubbleDataPoint, type Point } from "chart.js/auto";
|
||||
import type { BubbleDataPoint, Chart as ChartType, Point } from "chart.js";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface DonutChartProps {
|
||||
@@ -15,141 +16,180 @@ interface DonutChartProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default function DonutChart({ data, centerText }: DonutChartProps) {
|
||||
function DonutChartSkeleton() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<div className="animate-pulse flex items-center gap-8">
|
||||
<div className="w-40 h-40 rounded-full bg-gray-200 dark:bg-gray-700" />
|
||||
<div className="space-y-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-200 dark:bg-gray-700" />
|
||||
<div className="w-20 h-3 rounded bg-gray-200 dark:bg-gray-700" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DonutChartInner({ data, centerText }: DonutChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
const chartRef = useRef<ChartType<"doughnut"> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !data.values.length) return;
|
||||
let isMounted = true;
|
||||
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
async function initChart() {
|
||||
if (!ref.current || !data.values.length) return;
|
||||
|
||||
// Default colors if not provided
|
||||
const defaultColors: string[] = [
|
||||
"rgba(59, 130, 246, 0.8)", // blue
|
||||
"rgba(16, 185, 129, 0.8)", // green
|
||||
"rgba(249, 115, 22, 0.8)", // orange
|
||||
"rgba(236, 72, 153, 0.8)", // pink
|
||||
"rgba(139, 92, 246, 0.8)", // purple
|
||||
"rgba(107, 114, 128, 0.8)", // gray
|
||||
];
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const colors: string[] = data.colors || defaultColors;
|
||||
const { default: Chart } = await import("chart.js/auto");
|
||||
|
||||
// Helper to create an array of colors based on the data length
|
||||
const getColors = () => {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < data.values.length; i++) {
|
||||
result.push(colors[i % colors.length]);
|
||||
if (!isMounted) return;
|
||||
|
||||
// Default colors if not provided
|
||||
const defaultColors: string[] = [
|
||||
"rgba(59, 130, 246, 0.8)", // blue
|
||||
"rgba(16, 185, 129, 0.8)", // green
|
||||
"rgba(249, 115, 22, 0.8)", // orange
|
||||
"rgba(236, 72, 153, 0.8)", // pink
|
||||
"rgba(139, 92, 246, 0.8)", // purple
|
||||
"rgba(107, 114, 128, 0.8)", // gray
|
||||
];
|
||||
|
||||
const colors: string[] = data.colors || defaultColors;
|
||||
|
||||
// Helper to create an array of colors based on the data length
|
||||
const getColors = () => {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < data.values.length; i++) {
|
||||
result.push(colors[i % colors.length]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Destroy existing chart if any
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.values,
|
||||
backgroundColor: getColors(),
|
||||
borderWidth: 1,
|
||||
hoverOffset: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
cutout: "70%",
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
chartRef.current = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.values,
|
||||
backgroundColor: getColors(),
|
||||
borderWidth: 1,
|
||||
hoverOffset: 5,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue;
|
||||
const total = context.chart.data.datasets[0].data.reduce(
|
||||
(
|
||||
a: number,
|
||||
b:
|
||||
| number
|
||||
| Point
|
||||
| [number, number]
|
||||
| BubbleDataPoint
|
||||
| null
|
||||
) => {
|
||||
if (typeof b === "number") {
|
||||
return a + b;
|
||||
}
|
||||
// Handle other types like Point, [number, number], BubbleDataPoint if necessary
|
||||
// For now, we'll assume they don't contribute to the sum or are handled elsewhere
|
||||
return a;
|
||||
},
|
||||
0
|
||||
) as number;
|
||||
const percentage = Math.round((context.parsed * 100) / total);
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
cutout: "70%",
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue;
|
||||
const total = context.chart.data.datasets[0].data.reduce(
|
||||
(
|
||||
a: number,
|
||||
b:
|
||||
| number
|
||||
| Point
|
||||
| [number, number]
|
||||
| BubbleDataPoint
|
||||
| null
|
||||
) => {
|
||||
if (typeof b === "number") {
|
||||
return a + b;
|
||||
}
|
||||
return a;
|
||||
},
|
||||
0
|
||||
) as number;
|
||||
const percentage = Math.round((context.parsed * 100) / total);
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: centerText
|
||||
? [
|
||||
{
|
||||
id: "centerText",
|
||||
beforeDraw: (chart: Chart<"doughnut">) => {
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
plugins: centerText
|
||||
? [
|
||||
{
|
||||
id: "centerText",
|
||||
beforeDraw: (chart: ChartType<"doughnut">) => {
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
|
||||
// Calculate the actual chart area width (excluding legend)
|
||||
// Legend is positioned on the right, so we adjust the center X coordinate
|
||||
const chartArea = chart.chartArea;
|
||||
const chartWidth = chartArea.right - chartArea.left;
|
||||
const chartArea = chart.chartArea;
|
||||
const chartWidth = chartArea.right - chartArea.left;
|
||||
const centerX = chartArea.left + chartWidth / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Get the center of just the chart area (not including the legend)
|
||||
const centerX = chartArea.left + chartWidth / 2;
|
||||
const centerY = height / 2;
|
||||
if (centerText.title) {
|
||||
ctx.font = "1rem sans-serif";
|
||||
ctx.fillStyle = "#6B7280";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(centerText.title, centerX, centerY - 10);
|
||||
}
|
||||
|
||||
// Title text
|
||||
if (centerText.title) {
|
||||
ctx.font = "1rem sans-serif"; // Consistent font
|
||||
ctx.fillStyle = "#6B7280"; // Tailwind gray-500
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle"; // Align vertically
|
||||
ctx.fillText(centerText.title, centerX, centerY - 10); // Adjust Y offset
|
||||
}
|
||||
|
||||
// Value text
|
||||
if (centerText.value !== undefined) {
|
||||
ctx.font = "bold 1.5rem sans-serif"; // Consistent font, larger
|
||||
ctx.fillStyle = "#1F2937"; // Tailwind gray-800
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle"; // Align vertically
|
||||
ctx.fillText(
|
||||
centerText.value.toString(),
|
||||
centerX,
|
||||
centerY + 15
|
||||
); // Adjust Y offset
|
||||
}
|
||||
ctx.save();
|
||||
if (centerText.value !== undefined) {
|
||||
ctx.font = "bold 1.5rem sans-serif";
|
||||
ctx.fillStyle = "#1F2937";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(
|
||||
centerText.value.toString(),
|
||||
centerX,
|
||||
centerY + 15
|
||||
);
|
||||
}
|
||||
ctx.save();
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
});
|
||||
]
|
||||
: [],
|
||||
});
|
||||
}
|
||||
|
||||
return () => chart.destroy();
|
||||
initChart();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [data, centerText]);
|
||||
|
||||
return <canvas ref={ref} height={300} />;
|
||||
}
|
||||
|
||||
const DonutChart = dynamic(() => Promise.resolve(DonutChartInner), {
|
||||
ssr: false,
|
||||
loading: () => <DonutChartSkeleton />,
|
||||
});
|
||||
|
||||
export default DonutChart;
|
||||
|
||||
Reference in New Issue
Block a user