mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 08:52:10 +01:00
Add comprehensive dashboard features
Introduce company settings, user management, and layout components Implement session-based Company and User pages for admin access Integrate chart components for dynamic data visualization Add Sidebar for modular navigation Revamp global styles with Tailwind CSS Enhances user experience and administrative control
This commit is contained in:
@ -79,11 +79,11 @@ export default function GeographicMap({
|
||||
|
||||
// Process country data when client is ready and dependencies change
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
if (!isClient || !countries) return;
|
||||
|
||||
try {
|
||||
// Generate CountryData array for the Map component
|
||||
const data: CountryData[] = Object.entries(countries)
|
||||
const data: CountryData[] = Object.entries(countries || {})
|
||||
// Only include countries with known coordinates
|
||||
.filter(([code]) => {
|
||||
// If no coordinates found, log to help with debugging
|
||||
@ -115,8 +115,8 @@ export default function GeographicMap({
|
||||
}
|
||||
}, [countries, countryCoordinates, isClient]);
|
||||
|
||||
// Find the max count for scaling circles - handle empty countries object
|
||||
const countryValues = Object.values(countries);
|
||||
// Find the max count for scaling circles - handle empty or null countries object
|
||||
const countryValues = countries ? Object.values(countries) : [];
|
||||
const maxCount = countryValues.length > 0 ? Math.max(...countryValues, 1) : 1;
|
||||
|
||||
// Show loading state during SSR or until client-side rendering takes over
|
||||
|
||||
@ -7,28 +7,30 @@ import annotationPlugin from "chartjs-plugin-annotation";
|
||||
Chart.register(annotationPlugin);
|
||||
|
||||
interface ResponseTimeDistributionProps {
|
||||
responseTimes: number[];
|
||||
data: number[];
|
||||
average: number;
|
||||
targetResponseTime?: number;
|
||||
}
|
||||
|
||||
export default function ResponseTimeDistribution({
|
||||
responseTimes,
|
||||
data,
|
||||
average,
|
||||
targetResponseTime,
|
||||
}: ResponseTimeDistributionProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !responseTimes.length) return;
|
||||
if (!ref.current || !data || !data.length) return;
|
||||
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Create bins for the histogram (0-1s, 1-2s, 2-3s, etc.)
|
||||
const maxTime = Math.ceil(Math.max(...responseTimes));
|
||||
const maxTime = Math.ceil(Math.max(...data));
|
||||
const bins = Array(Math.min(maxTime + 1, 10)).fill(0);
|
||||
|
||||
// Count responses in each bin
|
||||
responseTimes.forEach((time) => {
|
||||
data.forEach((time) => {
|
||||
const binIndex = Math.min(Math.floor(time), bins.length - 1);
|
||||
bins[binIndex]++;
|
||||
});
|
||||
@ -63,26 +65,40 @@ export default function ResponseTimeDistribution({
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
annotation: targetResponseTime
|
||||
? {
|
||||
annotations: {
|
||||
targetLine: {
|
||||
annotation: {
|
||||
annotations: {
|
||||
averageLine: {
|
||||
type: "line",
|
||||
yMin: 0,
|
||||
yMax: Math.max(...bins),
|
||||
xMin: average,
|
||||
xMax: average,
|
||||
borderColor: "rgba(75, 192, 192, 1)",
|
||||
borderWidth: 2,
|
||||
label: {
|
||||
display: true,
|
||||
content: "Avg: " + average.toFixed(1) + "s",
|
||||
position: "start",
|
||||
},
|
||||
},
|
||||
targetLine: targetResponseTime
|
||||
? {
|
||||
type: "line",
|
||||
yMin: 0,
|
||||
yMax: Math.max(...bins),
|
||||
xMin: targetResponseTime,
|
||||
xMax: targetResponseTime,
|
||||
borderColor: "rgba(75, 192, 192, 1)",
|
||||
borderColor: "rgba(75, 192, 192, 0.7)",
|
||||
borderWidth: 2,
|
||||
label: {
|
||||
display: true,
|
||||
content: "Target",
|
||||
position: "start",
|
||||
position: "end",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
@ -103,7 +119,7 @@ export default function ResponseTimeDistribution({
|
||||
});
|
||||
|
||||
return () => chart.destroy();
|
||||
}, [responseTimes, targetResponseTime]);
|
||||
}, [data, average, targetResponseTime]);
|
||||
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
306
components/Sidebar.tsx
Normal file
306
components/Sidebar.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
// Icons for the sidebar
|
||||
const DashboardIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CompanyIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const UsersIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SessionsIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LogoutIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ToggleIcon = ({ isExpanded }: { isExpanded: boolean }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`h-5 w-5 transform transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface NavItemProps {
|
||||
href: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
isExpanded: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const NavItem: React.FC<NavItemProps> = ({
|
||||
href,
|
||||
label,
|
||||
icon,
|
||||
isExpanded,
|
||||
isActive,
|
||||
}) => (
|
||||
<Link
|
||||
href={href}
|
||||
className={`relative flex items-center p-3 my-1 rounded-lg transition-all group ${
|
||||
isActive
|
||||
? "bg-sky-100 text-sky-800 font-medium"
|
||||
: "hover:bg-gray-100 text-gray-700 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<span className={`flex-shrink-0 ${isExpanded ? "mr-3" : "mx-auto"}`}>
|
||||
{icon}
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<span className="truncate">{label}</span>
|
||||
) : (
|
||||
<div
|
||||
className="fixed ml-6 w-auto p-2 min-w-max rounded-md shadow-md text-xs font-medium
|
||||
text-white bg-gray-800 z-50
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
group-hover:visible group-hover:opacity-100 group-hover:translate-x-0"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default function Sidebar() {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const pathname = usePathname() || "";
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative h-screen bg-white shadow-md transition-all duration-300 ${
|
||||
isExpanded ? "w-56" : "w-16"
|
||||
} flex flex-col overflow-visible`}
|
||||
>
|
||||
{/* Logo section - now above toggle button */}
|
||||
<div className="flex flex-col items-center pt-5 pb-3">
|
||||
<div
|
||||
className={`relative ${isExpanded ? "w-16" : "w-10"} aspect-square mb-1`}
|
||||
>
|
||||
<img
|
||||
src="/favicon.svg"
|
||||
alt="LiveDash Logo"
|
||||
className="w-full h-full transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<span className="text-lg font-bold text-sky-700 mt-1">LiveDash</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toggle button */}
|
||||
<div className="flex justify-center border-b border-t py-2">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="p-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 transition-colors relative group"
|
||||
title={isExpanded ? "Collapse sidebar" : "Expand sidebar"}
|
||||
>
|
||||
<ToggleIcon isExpanded={isExpanded} />
|
||||
{!isExpanded && (
|
||||
<div
|
||||
className="fixed ml-6 w-auto p-2 min-w-max rounded-md shadow-md text-xs font-medium
|
||||
text-white bg-gray-800 z-50
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
group-hover:visible group-hover:opacity-100 group-hover:translate-x-0"
|
||||
>
|
||||
{isExpanded ? "Collapse sidebar" : "Expand sidebar"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation items */}
|
||||
<nav className="flex-1 py-4 px-2 overflow-y-auto overflow-x-visible">
|
||||
<NavItem
|
||||
href="/dashboard"
|
||||
label="Dashboard"
|
||||
icon={<DashboardIcon />}
|
||||
isExpanded={isExpanded}
|
||||
isActive={pathname === "/dashboard"}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/overview"
|
||||
label="Analytics"
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
isExpanded={isExpanded}
|
||||
isActive={pathname === "/dashboard/overview"}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/sessions"
|
||||
label="Sessions"
|
||||
icon={<SessionsIcon />}
|
||||
isExpanded={isExpanded}
|
||||
isActive={pathname.startsWith("/dashboard/sessions")}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/company"
|
||||
label="Company Settings"
|
||||
icon={<CompanyIcon />}
|
||||
isExpanded={isExpanded}
|
||||
isActive={pathname === "/dashboard/company"}
|
||||
/>
|
||||
<NavItem
|
||||
href="/dashboard/users"
|
||||
label="User Management"
|
||||
icon={<UsersIcon />}
|
||||
isExpanded={isExpanded}
|
||||
isActive={pathname === "/dashboard/users"}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{/* Logout at the bottom */}
|
||||
<div className="p-4 border-t mt-auto">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={`relative flex items-center p-3 w-full rounded-lg text-gray-700 hover:bg-gray-100 hover:text-gray-900 transition-all group ${
|
||||
isExpanded ? "" : "justify-center"
|
||||
}`}
|
||||
>
|
||||
<span className={`flex-shrink-0 ${isExpanded ? "mr-3" : ""}`}>
|
||||
<LogoutIcon />
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<span>Logout</span>
|
||||
) : (
|
||||
<div
|
||||
className="fixed ml-6 w-auto p-2 min-w-max rounded-md shadow-md text-xs font-medium
|
||||
text-white bg-gray-800 z-50
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
group-hover:visible group-hover:opacity-100 group-hover:translate-x-0"
|
||||
>
|
||||
Logout
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user