refactor: apply React best practices and migrate to ESLint CLI

- Dynamic import for WordCloud component (code splitting)
- Fix waterfall in handleRefresh with Promise.all
- Memoize data prep functions in overview page (7 useMemo hooks)
- Replace useState+useEffect with useMemo in CountryDisplay/LanguageDisplay
- Memoize navigationCards and class getters in dashboard page
- Extract inline handlers with useCallback
- Fix missing index parameter in TopQuestionsChart map
- Migrate from next lint to ESLint CLI (Next.js 16 deprecation)
This commit is contained in:
2026-01-20 07:05:27 +01:00
parent 5042a6c016
commit 8b3846539f
7 changed files with 298 additions and 288 deletions

View File

@@ -12,7 +12,7 @@ import {
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { type FC, useEffect, useState } from "react";
import { type FC, useCallback, useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -31,6 +31,98 @@ const DashboardPage: FC = () => {
}
}, [status, router]);
// Memoize navigation cards - only recalculates when user role changes
const navigationCards = useMemo(() => {
const baseCards = [
{
title: "Analytics Overview",
description:
"View comprehensive metrics, charts, and insights from your chat sessions",
icon: <BarChart3 className="h-6 w-6" />,
href: "/dashboard/overview",
variant: "primary" as const,
features: ["Real-time metrics", "Interactive charts", "Trend analysis"],
adminOnly: false,
},
{
title: "Session Browser",
description:
"Browse, search, and analyze individual conversation sessions",
icon: <MessageSquare className="h-6 w-6" />,
href: "/dashboard/sessions",
variant: "success" as const,
features: ["Session search", "Conversation details", "Export data"],
adminOnly: false,
},
];
if (session?.user?.role === "ADMIN") {
return [
...baseCards,
{
title: "Company Settings",
description:
"Configure company settings, integrations, and API connections",
icon: <Settings className="h-6 w-6" />,
href: "/dashboard/company",
variant: "warning" as const,
features: [
"API configuration",
"Integration settings",
"Data management",
],
adminOnly: true,
},
{
title: "User Management",
description:
"Invite team members and manage user accounts and permissions",
icon: <Users className="h-6 w-6" />,
href: "/dashboard/users",
variant: "default" as const,
features: ["User invitations", "Role management", "Access control"],
adminOnly: true,
},
];
}
return baseCards;
}, [session?.user?.role]);
// Memoize class getter functions
const getCardClasses = useCallback((variant: string) => {
switch (variant) {
case "primary":
return "border-primary/20 bg-linear-to-br from-primary/5 to-primary/10 hover:from-primary/10 hover:to-primary/15";
case "success":
return "border-green-200 bg-linear-to-br from-green-50 to-green-100 hover:from-green-100 hover:to-green-150 dark:border-green-800 dark:from-green-950 dark:to-green-900";
case "warning":
return "border-amber-200 bg-linear-to-br from-amber-50 to-amber-100 hover:from-amber-100 hover:to-amber-150 dark:border-amber-800 dark:from-amber-950 dark:to-amber-900";
default:
return "border-border bg-linear-to-br from-card to-muted/20 hover:from-muted/30 hover:to-muted/40";
}
}, []);
const getIconClasses = useCallback((variant: string) => {
switch (variant) {
case "primary":
return "bg-primary/10 text-primary border-primary/20";
case "success":
return "bg-green-100 text-green-600 border-green-200 dark:bg-green-900 dark:text-green-400 dark:border-green-800";
case "warning":
return "bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900 dark:text-amber-400 dark:border-amber-800";
default:
return "bg-muted text-muted-foreground border-border";
}
}, []);
// Navigate handler to avoid creating new functions on each render
const handleNavigate = useCallback(
(href: string) => {
router.push(href);
},
[router]
);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
@@ -47,81 +139,6 @@ const DashboardPage: FC = () => {
);
}
const navigationCards = [
{
title: "Analytics Overview",
description:
"View comprehensive metrics, charts, and insights from your chat sessions",
icon: <BarChart3 className="h-6 w-6" />,
href: "/dashboard/overview",
variant: "primary" as const,
features: ["Real-time metrics", "Interactive charts", "Trend analysis"],
},
{
title: "Session Browser",
description:
"Browse, search, and analyze individual conversation sessions",
icon: <MessageSquare className="h-6 w-6" />,
href: "/dashboard/sessions",
variant: "success" as const,
features: ["Session search", "Conversation details", "Export data"],
},
...(session?.user?.role === "ADMIN"
? [
{
title: "Company Settings",
description:
"Configure company settings, integrations, and API connections",
icon: <Settings className="h-6 w-6" />,
href: "/dashboard/company",
variant: "warning" as const,
features: [
"API configuration",
"Integration settings",
"Data management",
],
adminOnly: true,
},
{
title: "User Management",
description:
"Invite team members and manage user accounts and permissions",
icon: <Users className="h-6 w-6" />,
href: "/dashboard/users",
variant: "default" as const,
features: ["User invitations", "Role management", "Access control"],
adminOnly: true,
},
]
: []),
];
const getCardClasses = (variant: string) => {
switch (variant) {
case "primary":
return "border-primary/20 bg-linear-to-br from-primary/5 to-primary/10 hover:from-primary/10 hover:to-primary/15";
case "success":
return "border-green-200 bg-linear-to-br from-green-50 to-green-100 hover:from-green-100 hover:to-green-150 dark:border-green-800 dark:from-green-950 dark:to-green-900";
case "warning":
return "border-amber-200 bg-linear-to-br from-amber-50 to-amber-100 hover:from-amber-100 hover:to-amber-150 dark:border-amber-800 dark:from-amber-950 dark:to-amber-900";
default:
return "border-border bg-linear-to-br from-card to-muted/20 hover:from-muted/30 hover:to-muted/40";
}
};
const getIconClasses = (variant: string) => {
switch (variant) {
case "primary":
return "bg-primary/10 text-primary border-primary/20";
case "success":
return "bg-green-100 text-green-600 border-green-200 dark:bg-green-900 dark:text-green-400 dark:border-green-800";
case "warning":
return "bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900 dark:text-amber-400 dark:border-amber-800";
default:
return "bg-muted text-muted-foreground border-border";
}
};
return (
<div className="space-y-8">
{/* Welcome Header */}
@@ -163,7 +180,7 @@ const DashboardPage: FC = () => {
className={`relative overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-1 cursor-pointer group ${getCardClasses(
card.variant
)}`}
onClick={() => router.push(card.href)}
onClick={() => handleNavigate(card.href)}
>
{/* Subtle gradient overlay */}
<div className="absolute inset-0 bg-linear-to-br from-white/50 to-transparent dark:from-white/5 pointer-events-none" />
@@ -219,7 +236,7 @@ const DashboardPage: FC = () => {
variant={card.variant === "primary" ? "default" : "outline"}
onClick={(e) => {
e.stopPropagation();
router.push(card.href);
handleNavigate(card.href);
}}
>
<span>