mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-13 19:15:44 +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,7 +1,9 @@
|
||||
"use client";
|
||||
import Chart from "chart.js/auto";
|
||||
|
||||
import type { Chart as ChartType } from "chart.js";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getLocalizedLanguageName } from "../lib/localization"; // Corrected import path
|
||||
import { getLocalizedLanguageName } from "../lib/localization";
|
||||
|
||||
interface SessionsData {
|
||||
[date: string]: number;
|
||||
@@ -43,266 +45,432 @@ interface TokenUsageChartProps {
|
||||
};
|
||||
}
|
||||
|
||||
// Basic line and bar chart for metrics. Extend as needed.
|
||||
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !sessionsPerDay) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: Object.keys(sessionsPerDay),
|
||||
datasets: [
|
||||
{
|
||||
label: "Sessions",
|
||||
data: Object.values(sessionsPerDay),
|
||||
borderColor: "rgb(59, 130, 246)",
|
||||
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } },
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [sessionsPerDay]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
// Loading skeleton for charts
|
||||
function ChartSkeleton({ height = 180 }: { height?: number }) {
|
||||
return (
|
||||
<div
|
||||
className="animate-pulse bg-gray-200 dark:bg-gray-700 rounded"
|
||||
style={{ height }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !categories) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: Object.keys(categories),
|
||||
datasets: [
|
||||
{
|
||||
label: "Categories",
|
||||
data: Object.values(categories),
|
||||
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } },
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [categories]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
function DonutChartSkeleton() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[180px]">
|
||||
<div className="animate-pulse flex items-center gap-6">
|
||||
<div className="w-28 h-28 rounded-full bg-gray-200 dark:bg-gray-700" />
|
||||
<div className="space-y-2">
|
||||
{[...Array(3)].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-16 h-3 rounded bg-gray-200 dark:bg-gray-700" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SentimentChart({ sentimentData }: SentimentChartProps) {
|
||||
// Inner components that do the actual rendering
|
||||
function SessionsLineChartInner({ sessionsPerDay }: SessionsLineChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !sentimentData) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const chartRef = useRef<ChartType | null>(null);
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Positive", "Neutral", "Negative"],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
sentimentData.positive,
|
||||
sentimentData.neutral,
|
||||
sentimentData.negative,
|
||||
],
|
||||
backgroundColor: [
|
||||
"rgba(34, 197, 94, 0.8)", // green
|
||||
"rgba(249, 115, 22, 0.8)", // orange
|
||||
"rgba(239, 68, 68, 0.8)", // red
|
||||
],
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function initChart() {
|
||||
if (!ref.current || !sessionsPerDay) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const { default: Chart } = await import("chart.js/auto");
|
||||
if (!isMounted) return;
|
||||
|
||||
if (chartRef.current) chartRef.current.destroy();
|
||||
|
||||
chartRef.current = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: Object.keys(sessionsPerDay),
|
||||
datasets: [
|
||||
{
|
||||
label: "Sessions",
|
||||
data: Object.values(sessionsPerDay),
|
||||
borderColor: "rgb(59, 130, 246)",
|
||||
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
cutout: "65%",
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [sentimentData]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
export function LanguagePieChart({ languages }: LanguagePieChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !languages) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Get top 5 languages, combine others
|
||||
const entries = Object.entries(languages);
|
||||
const topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
|
||||
// Sum the count of all other languages
|
||||
const otherCount = entries
|
||||
.slice(5)
|
||||
.reduce((sum, [, count]) => sum + count, 0);
|
||||
if (otherCount > 0) {
|
||||
topLanguages.push(["Other", otherCount]);
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Store original ISO codes for tooltip
|
||||
const isoCodes = topLanguages.map(([lang]) => lang);
|
||||
initChart();
|
||||
|
||||
const labels = topLanguages.map(([lang]) => {
|
||||
if (lang === "Other") {
|
||||
return "Other";
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
// Use getLocalizedLanguageName for robust name resolution
|
||||
// Pass "en" to maintain consistency with previous behavior if navigator.language is different
|
||||
return getLocalizedLanguageName(lang, "en");
|
||||
});
|
||||
};
|
||||
}, [sessionsPerDay]);
|
||||
|
||||
const data = topLanguages.map(([, count]) => count);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: [
|
||||
"rgba(59, 130, 246, 0.8)",
|
||||
"rgba(16, 185, 129, 0.8)",
|
||||
"rgba(249, 115, 22, 0.8)",
|
||||
"rgba(236, 72, 153, 0.8)",
|
||||
"rgba(139, 92, 246, 0.8)",
|
||||
"rgba(107, 114, 128, 0.8)",
|
||||
],
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
function CategoriesBarChartInner({ categories }: CategoriesBarChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
const chartRef = useRef<ChartType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function initChart() {
|
||||
if (!ref.current || !categories) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const { default: Chart } = await import("chart.js/auto");
|
||||
if (!isMounted) return;
|
||||
|
||||
if (chartRef.current) chartRef.current.destroy();
|
||||
|
||||
chartRef.current = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: Object.keys(categories),
|
||||
datasets: [
|
||||
{
|
||||
label: "Categories",
|
||||
data: Object.values(categories),
|
||||
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
initChart();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [categories]);
|
||||
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
function SentimentChartInner({ sentimentData }: SentimentChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
const chartRef = useRef<ChartType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function initChart() {
|
||||
if (!ref.current || !sentimentData) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const { default: Chart } = await import("chart.js/auto");
|
||||
if (!isMounted) return;
|
||||
|
||||
if (chartRef.current) chartRef.current.destroy();
|
||||
|
||||
chartRef.current = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Positive", "Neutral", "Negative"],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
sentimentData.positive,
|
||||
sentimentData.neutral,
|
||||
sentimentData.negative,
|
||||
],
|
||||
backgroundColor: [
|
||||
"rgba(34, 197, 94, 0.8)",
|
||||
"rgba(249, 115, 22, 0.8)",
|
||||
"rgba(239, 68, 68, 0.8)",
|
||||
],
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue || "";
|
||||
const index = context.dataIndex;
|
||||
const originalIsoCode = isoCodes[index]; // Get the original code
|
||||
cutout: "65%",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Only show ISO code if it's not "Other"
|
||||
// and it's a valid 2-letter code (check lowercase version)
|
||||
if (
|
||||
originalIsoCode &&
|
||||
originalIsoCode !== "Other" &&
|
||||
/^[a-z]{2}$/.test(originalIsoCode.toLowerCase())
|
||||
) {
|
||||
return `${label} (${originalIsoCode.toUpperCase()}): ${value}`;
|
||||
}
|
||||
initChart();
|
||||
|
||||
return `${label}: ${value}`;
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [sentimentData]);
|
||||
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
function LanguagePieChartInner({ languages }: LanguagePieChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
const chartRef = useRef<ChartType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function initChart() {
|
||||
if (!ref.current || !languages) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const { default: Chart } = await import("chart.js/auto");
|
||||
if (!isMounted) return;
|
||||
|
||||
// Get top 5 languages, combine others
|
||||
const entries = Object.entries(languages);
|
||||
const topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
|
||||
const otherCount = entries
|
||||
.slice(5)
|
||||
.reduce((sum, [, count]) => sum + count, 0);
|
||||
if (otherCount > 0) {
|
||||
topLanguages.push(["Other", otherCount]);
|
||||
}
|
||||
|
||||
const isoCodes = topLanguages.map(([lang]) => lang);
|
||||
|
||||
const labels = topLanguages.map(([lang]) => {
|
||||
if (lang === "Other") return "Other";
|
||||
return getLocalizedLanguageName(lang, "en");
|
||||
});
|
||||
|
||||
const data = topLanguages.map(([, count]) => count);
|
||||
|
||||
if (chartRef.current) chartRef.current.destroy();
|
||||
|
||||
chartRef.current = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: [
|
||||
"rgba(59, 130, 246, 0.8)",
|
||||
"rgba(16, 185, 129, 0.8)",
|
||||
"rgba(249, 115, 22, 0.8)",
|
||||
"rgba(236, 72, 153, 0.8)",
|
||||
"rgba(139, 92, 246, 0.8)",
|
||||
"rgba(107, 114, 128, 0.8)",
|
||||
],
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue || "";
|
||||
const index = context.dataIndex;
|
||||
const originalIsoCode = isoCodes[index];
|
||||
|
||||
if (
|
||||
originalIsoCode &&
|
||||
originalIsoCode !== "Other" &&
|
||||
/^[a-z]{2}$/.test(originalIsoCode.toLowerCase())
|
||||
) {
|
||||
return `${label} (${originalIsoCode.toUpperCase()}): ${value}`;
|
||||
}
|
||||
|
||||
return `${label}: ${value}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
initChart();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [languages]);
|
||||
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
export function TokenUsageChart({ tokenData }: TokenUsageChartProps) {
|
||||
function TokenUsageChartInner({ tokenData }: TokenUsageChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !tokenData) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const chartRef = useRef<ChartType | null>(null);
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: tokenData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Tokens",
|
||||
data: tokenData.values,
|
||||
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
||||
borderWidth: 1,
|
||||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
label: "Cost (EUR)",
|
||||
data: tokenData.costs,
|
||||
backgroundColor: "rgba(16, 185, 129, 0.7)",
|
||||
borderWidth: 1,
|
||||
type: "line",
|
||||
yAxisID: "y1",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: true } },
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
position: "left",
|
||||
title: {
|
||||
display: true,
|
||||
text: "Token Count",
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function initChart() {
|
||||
if (!ref.current || !tokenData) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const { default: Chart } = await import("chart.js/auto");
|
||||
if (!isMounted) return;
|
||||
|
||||
if (chartRef.current) chartRef.current.destroy();
|
||||
|
||||
chartRef.current = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: tokenData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Tokens",
|
||||
data: tokenData.values,
|
||||
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
||||
borderWidth: 1,
|
||||
yAxisID: "y",
|
||||
},
|
||||
},
|
||||
y1: {
|
||||
beginAtZero: true,
|
||||
position: "right",
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
{
|
||||
label: "Cost (EUR)",
|
||||
data: tokenData.costs,
|
||||
backgroundColor: "rgba(16, 185, 129, 0.7)",
|
||||
borderWidth: 1,
|
||||
type: "line",
|
||||
yAxisID: "y1",
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Cost (EUR)",
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: true } },
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
position: "left",
|
||||
title: {
|
||||
display: true,
|
||||
text: "Token Count",
|
||||
},
|
||||
},
|
||||
y1: {
|
||||
beginAtZero: true,
|
||||
position: "right",
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Cost (EUR)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
initChart();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [tokenData]);
|
||||
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
// Dynamic exports with SSR disabled
|
||||
export const SessionsLineChart = dynamic(
|
||||
() => Promise.resolve(SessionsLineChartInner),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <ChartSkeleton height={180} />,
|
||||
}
|
||||
);
|
||||
|
||||
export const CategoriesBarChart = dynamic(
|
||||
() => Promise.resolve(CategoriesBarChartInner),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <ChartSkeleton height={180} />,
|
||||
}
|
||||
);
|
||||
|
||||
export const SentimentChart = dynamic(
|
||||
() => Promise.resolve(SentimentChartInner),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <DonutChartSkeleton />,
|
||||
}
|
||||
);
|
||||
|
||||
export const LanguagePieChart = dynamic(
|
||||
() => Promise.resolve(LanguagePieChartInner),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <DonutChartSkeleton />,
|
||||
}
|
||||
);
|
||||
|
||||
export const TokenUsageChart = dynamic(
|
||||
() => Promise.resolve(TokenUsageChartInner),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <ChartSkeleton height={180} />,
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user