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:
2026-01-20 07:28:10 +01:00
parent 8b3846539f
commit 5bfd762e55
161 changed files with 14655 additions and 11682 deletions

View File

@@ -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} />,
}
);