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

@@ -0,0 +1,97 @@
"use client";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
interface BarChartData {
name: string;
value: number;
[key: string]: string | number;
}
interface BarChartInnerProps {
data: BarChartData[];
dataKey: string;
colors: string[];
height: number;
}
interface TooltipProps {
active?: boolean;
payload?: Array<{ value: number; name?: string }>;
label?: string;
}
const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
sessions
</p>
</div>
);
}
return null;
};
export default function BarChartInner({
data,
dataKey,
colors,
height,
}: BarChartInnerProps) {
return (
<ResponsiveContainer width="100%" height={height}>
<BarChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="name"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey={dataKey}
radius={[4, 4, 0, 0]}
className="transition-all duration-200"
>
{data.map((entry, index) => (
<Cell
key={`cell-${entry.name}-${index}`}
fill={colors[index % colors.length]}
className="hover:opacity-80"
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}

View File

@@ -1,15 +1,6 @@
"use client";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface BarChartData {
@@ -27,28 +18,24 @@ interface BarChartProps {
className?: string;
}
interface TooltipProps {
active?: boolean;
payload?: Array<{ value: number; name?: string }>;
label?: string;
}
const ChartSkeleton = ({ height = 300 }: { height?: number }) => (
<div className="animate-pulse" style={{ height }}>
<div className="h-full w-full bg-muted rounded-md flex items-end justify-around px-4 pb-8">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="bg-muted-foreground/20 rounded-t"
style={{ width: "12%", height: `${20 + Math.random() * 60}%` }}
/>
))}
</div>
</div>
);
const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
sessions
</p>
</div>
);
}
return null;
};
const BarChartInner = dynamic(() => import("./bar-chart-inner"), {
ssr: false,
loading: () => <ChartSkeleton />,
});
export default function ModernBarChart({
data,
@@ -72,48 +59,12 @@ export default function ModernBarChart({
</CardHeader>
)}
<CardContent>
<ResponsiveContainer width="100%" height={height}>
<BarChart
data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="name"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey={dataKey}
radius={[4, 4, 0, 0]}
className="transition-all duration-200"
>
{data.map((entry, index) => (
<Cell
key={`cell-${entry.name}-${index}`}
fill={colors[index % colors.length]}
className="hover:opacity-80"
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<BarChartInner
data={data}
dataKey={dataKey}
colors={colors}
height={height}
/>
</CardContent>
</Card>
);

View File

@@ -0,0 +1,146 @@
"use client";
import {
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
} from "recharts";
interface DonutChartInnerProps {
data: Array<{ name: string; value: number; color?: string }>;
title?: string;
centerText?: {
title: string;
value: string | number;
};
colors: string[];
height: number;
}
interface TooltipProps {
active?: boolean;
payload?: Array<{
name: string;
value: number;
payload: { total: number };
}>;
}
const CustomTooltip = ({ active, payload }: TooltipProps) => {
if (active && payload && payload.length) {
const data = payload[0];
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{data.name}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">{data.value}</span>{" "}
sessions ({((data.value / data.payload.total) * 100).toFixed(1)}%)
</p>
</div>
);
}
return null;
};
interface LegendProps {
payload?: Array<{
value: string;
color: string;
type?: string;
}>;
}
const CustomLegend = ({ payload }: LegendProps) => {
return (
<div className="flex flex-wrap justify-center gap-4 mt-4">
{payload?.map((entry, index) => (
<div
key={`legend-${entry.value}-${index}`}
className="flex items-center gap-2"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm text-muted-foreground">{entry.value}</span>
</div>
))}
</div>
);
};
interface CenterLabelProps {
centerText?: {
title: string;
value: string | number;
};
}
const CenterLabel = ({ centerText }: CenterLabelProps) => {
if (!centerText) return null;
return (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="text-center">
<p className="text-2xl font-bold">{centerText.value}</p>
<p className="text-sm text-muted-foreground">{centerText.title}</p>
</div>
</div>
);
};
export default function DonutChartInner({
data,
title,
centerText,
colors,
height,
}: DonutChartInnerProps) {
const total = data.reduce((sum, item) => sum + item.value, 0);
const dataWithTotal = data.map((item) => ({ ...item, total }));
return (
<div
className="relative"
role="img"
aria-label={`${title || "Chart"} - ${data.length} segments`}
>
<ResponsiveContainer width="100%" height={height}>
<PieChart>
<Pie
data={dataWithTotal}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
dataKey="value"
className="transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
}
}}
>
{dataWithTotal.map((entry, index) => (
<Cell
key={`cell-${entry.name}-${index}`}
fill={entry.color || colors[index % colors.length]}
className="hover:opacity-80 cursor-pointer focus:opacity-80"
stroke="hsl(var(--background))"
strokeWidth={2}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
<CenterLabel centerText={centerText} />
</div>
);
}

View File

@@ -1,13 +1,6 @@
"use client";
import {
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
} from "recharts";
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface DonutChartProps {
@@ -22,78 +15,25 @@ interface DonutChartProps {
className?: string;
}
interface TooltipProps {
active?: boolean;
payload?: Array<{
name: string;
value: number;
payload: { total: number };
}>;
}
const CustomTooltip = ({ active, payload }: TooltipProps) => {
if (active && payload && payload.length) {
const data = payload[0];
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{data.name}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">{data.value}</span>{" "}
sessions ({((data.value / data.payload.total) * 100).toFixed(1)}%)
</p>
</div>
);
}
return null;
};
interface LegendProps {
payload?: Array<{
value: string;
color: string;
type?: string;
}>;
}
const CustomLegend = ({ payload }: LegendProps) => {
return (
<div className="flex flex-wrap justify-center gap-4 mt-4">
{payload?.map((entry, index) => (
<div
key={`legend-${entry.value}-${index}`}
className="flex items-center gap-2"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm text-muted-foreground">{entry.value}</span>
</div>
))}
const ChartSkeleton = ({ height = 300 }: { height?: number }) => (
<div className="animate-pulse relative" style={{ height }}>
<div className="absolute inset-0 flex items-center justify-center">
<div
className="rounded-full bg-muted-foreground/20"
style={{ width: "200px", height: "200px" }}
/>
<div
className="absolute rounded-full bg-background"
style={{ width: "120px", height: "120px" }}
/>
</div>
);
};
</div>
);
interface CenterLabelProps {
centerText?: {
title: string;
value: string | number;
};
total: number;
}
const CenterLabel = ({ centerText }: CenterLabelProps) => {
if (!centerText) return null;
return (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="text-center">
<p className="text-2xl font-bold">{centerText.value}</p>
<p className="text-sm text-muted-foreground">{centerText.title}</p>
</div>
</div>
);
};
const DonutChartInner = dynamic(() => import("./donut-chart-inner"), {
ssr: false,
loading: () => <ChartSkeleton />,
});
export default function ModernDonutChart({
data,
@@ -109,9 +49,6 @@ export default function ModernDonutChart({
height = 300,
className,
}: DonutChartProps) {
const total = data.reduce((sum, item) => sum + item.value, 0);
const dataWithTotal = data.map((item) => ({ ...item, total }));
return (
<Card className={className}>
{title && (
@@ -120,45 +57,13 @@ export default function ModernDonutChart({
</CardHeader>
)}
<CardContent>
<div
className="relative"
role="img"
aria-label={`${title || "Chart"} - ${data.length} segments`}
>
<ResponsiveContainer width="100%" height={height}>
<PieChart>
<Pie
data={dataWithTotal}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
dataKey="value"
className="transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
}
}}
>
{dataWithTotal.map((entry, index) => (
<Cell
key={`cell-${entry.name}-${index}`}
fill={entry.color || colors[index % colors.length]}
className="hover:opacity-80 cursor-pointer focus:opacity-80"
stroke="hsl(var(--background))"
strokeWidth={2}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
<CenterLabel centerText={centerText} total={total} />
</div>
<DonutChartInner
data={data}
title={title}
centerText={centerText}
colors={colors}
height={height}
/>
</CardContent>
</Card>
);

View File

@@ -0,0 +1,120 @@
"use client";
import { useId } from "react";
import {
Area,
AreaChart,
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
interface LineChartData {
date: string;
value: number;
[key: string]: string | number;
}
interface LineChartInnerProps {
data: LineChartData[];
dataKey: string;
color: string;
gradient: boolean;
height: number;
}
interface TooltipProps {
active?: boolean;
payload?: Array<{ value: number; name?: string }>;
label?: string;
}
const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
sessions
</p>
</div>
);
}
return null;
};
export default function LineChartInner({
data,
dataKey,
color,
gradient,
height,
}: LineChartInnerProps) {
const gradientId = useId();
const ChartComponent = gradient ? AreaChart : LineChart;
return (
<ResponsiveContainer width="100%" height={height}>
<ChartComponent
data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<defs>
{gradient && (
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
<stop offset="95%" stopColor={color} stopOpacity={0.05} />
</linearGradient>
)}
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="date"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
{gradient ? (
<Area
type="monotone"
dataKey={dataKey}
stroke={color}
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: color, strokeWidth: 2 }}
/>
) : (
<Line
type="monotone"
dataKey={dataKey}
stroke={color}
strokeWidth={2}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: color, strokeWidth: 2 }}
/>
)}
</ChartComponent>
</ResponsiveContainer>
);
}

View File

@@ -1,17 +1,6 @@
"use client";
import { useId } from "react";
import {
Area,
AreaChart,
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface LineChartData {
@@ -30,28 +19,26 @@ interface LineChartProps {
className?: string;
}
interface TooltipProps {
active?: boolean;
payload?: Array<{ value: number; name?: string }>;
label?: string;
}
const ChartSkeleton = ({ height = 300 }: { height?: number }) => (
<div className="animate-pulse" style={{ height }}>
<div className="h-full w-full bg-muted rounded-md relative overflow-hidden">
<svg className="w-full h-full" preserveAspectRatio="none">
<path
d="M0,150 Q50,100 100,120 T200,80 T300,100 T400,60 T500,90"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="text-muted-foreground/20"
/>
</svg>
</div>
</div>
);
const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
sessions
</p>
</div>
);
}
return null;
};
const LineChartInner = dynamic(() => import("./line-chart-inner"), {
ssr: false,
loading: () => <ChartSkeleton />,
});
export default function ModernLineChart({
data,
@@ -62,9 +49,6 @@ export default function ModernLineChart({
height = 300,
className,
}: LineChartProps) {
const gradientId = useId();
const ChartComponent = gradient ? AreaChart : LineChart;
return (
<Card className={className}>
{title && (
@@ -73,61 +57,13 @@ export default function ModernLineChart({
</CardHeader>
)}
<CardContent>
<ResponsiveContainer width="100%" height={height}>
<ChartComponent
data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<defs>
{gradient && (
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
<stop offset="95%" stopColor={color} stopOpacity={0.05} />
</linearGradient>
)}
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="date"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
{gradient ? (
<Area
type="monotone"
dataKey={dataKey}
stroke={color}
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: color, strokeWidth: 2 }}
/>
) : (
<Line
type="monotone"
dataKey={dataKey}
stroke={color}
strokeWidth={2}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: color, strokeWidth: 2 }}
/>
)}
</ChartComponent>
</ResponsiveContainer>
<LineChartInner
data={data}
dataKey={dataKey}
color={color}
gradient={gradient}
height={height}
/>
</CardContent>
</Card>
);