refactor: redesign table module

This commit is contained in:
2026-03-18 01:03:58 +05:30
parent caf527f584
commit 344f37719e
24 changed files with 991 additions and 815 deletions

View File

@@ -1,159 +1,156 @@
"use client"
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
import * as React from "react"
import { Loader2 } from "lucide-react"
import { Label, Pie, PieChart } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
interface DetectionDonutChartProps {
defectedSignboard: number
pothole: number
roadCrack: number
damagedRoadMarking: number
goodSignboard: number
isLoading?: boolean
defectedSignboard: number
pothole: number
roadCrack: number
damagedRoadMarking: number
goodSignboard: number
isLoading?: boolean
}
const COLORS = {
defectedSignboard: "#3b82f6", // Blue
pothole: "#ef4444", // Red
roadCrack: "#f59e0b", // Amber/Orange
damagedRoadMarking: "#6366f1", // Indigo
goodSignboard: "#10b981" // Emerald
}
const chartConfig = {
pothole: {
label: "Potholes",
color: "var(--chart-1)",
},
defectedSignboard: {
label: "Defected Signboards",
color: "var(--chart-2)",
},
roadCrack: {
label: "Road Cracks",
color: "var(--chart-3)",
},
damagedRoadMarking: {
label: "Damaged Markings",
color: "var(--chart-4)",
},
goodSignboard: {
label: "Good Signboards",
color: "var(--chart-5)",
},
} satisfies ChartConfig
export function DetectionDonutChart({
defectedSignboard,
pothole,
roadCrack,
damagedRoadMarking,
goodSignboard,
isLoading = false
defectedSignboard,
pothole,
roadCrack,
damagedRoadMarking,
goodSignboard,
isLoading = false,
}: DetectionDonutChartProps) {
if (isLoading) {
return (
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50" />
</div>
)
}
const chartData = React.useMemo(
() =>
[
{ type: "pothole", count: pothole, fill: "var(--color-pothole)" },
{
type: "defectedSignboard",
count: defectedSignboard,
fill: "var(--color-defectedSignboard)",
},
{ type: "roadCrack", count: roadCrack, fill: "var(--color-roadCrack)" },
{
type: "damagedRoadMarking",
count: damagedRoadMarking,
fill: "var(--color-damagedRoadMarking)",
},
{
type: "goodSignboard",
count: goodSignboard,
fill: "var(--color-goodSignboard)",
},
].filter((item) => item.count > 0),
[
pothole,
defectedSignboard,
roadCrack,
damagedRoadMarking,
goodSignboard,
]
)
const total = defectedSignboard + pothole + roadCrack + damagedRoadMarking + goodSignboard
if (total === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No detections found</p>
<p className="text-xs mt-1">Process videos to see data</p>
</div>
)
}
const data = [
{ name: "Defected Signboards", value: defectedSignboard, color: COLORS.defectedSignboard },
{ name: "Potholes", value: pothole, color: COLORS.pothole },
{ name: "Road Cracks", value: roadCrack, color: COLORS.roadCrack },
{ name: "Damaged Markings", value: damagedRoadMarking, color: COLORS.damagedRoadMarking },
{ name: "Good Signboards", value: goodSignboard, color: COLORS.goodSignboard }
].filter(item => item.value > 0)
const totalDetections = React.useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.count, 0)
}, [chartData])
if (isLoading) {
return (
<div className="h-[220px] relative">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<defs>
<linearGradient id="gradPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ff8a8a" />
<stop offset="100%" stopColor="#ef4444" />
</linearGradient>
<linearGradient id="gradDefectedSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" />
<stop offset="100%" stopColor="#3b82f6" />
</linearGradient>
<linearGradient id="gradCrack" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbbf24" />
<stop offset="100%" stopColor="#f59e0b" />
</linearGradient>
<linearGradient id="gradMarking" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#818cf8" />
<stop offset="100%" stopColor="#6366f1" />
</linearGradient>
<linearGradient id="gradGoodSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#34d399" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
<Pie
data={data}
cx="50%"
cy="45%"
innerRadius={50}
outerRadius={70}
paddingAngle={5}
dataKey="value"
strokeWidth={0}
isAnimationActive={false}
>
{data.map((entry, index) => {
const gradId = entry.name === "Potholes" ? "gradPothole" :
entry.name === "Defected Signboards" ? "gradDefectedSign" :
entry.name === "Road Cracks" ? "gradCrack" :
entry.name === "Damaged Markings" ? "gradMarking" : "gradGoodSign"
return (
<Cell
key={`cell-${index}`}
fill={`url(#${gradId})`}
fillOpacity={1}
className="hover:fill-opacity-80"
/>
)
})}
</Pie>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0]
return (
<div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg">
<p className="font-medium text-sm">{data.name}</p>
<p className="text-sm text-muted-foreground">
Count: <span className="font-semibold text-foreground">{data.value}</span>
</p>
<p className="text-xs text-muted-foreground">
{((Number(data.value) / total) * 100).toFixed(1)}% of total
</p>
</div>
)
}
return null
}}
/>
<Legend
verticalAlign="bottom"
height={40}
content={({ payload }) => (
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mt-2">
{payload?.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-1.5">
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
{entry.value}: {data[index].value}
</span>
</div>
))}
</div>
)}
/>
</PieChart>
</ResponsiveContainer>
{/* Center label */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-55px' }}>
<div className="text-center">
<p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
<p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>
</div>
</div>
</div>
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
</div>
)
}
if (totalDetections === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No detections found</p>
<p className="text-xs mt-1">Process videos to see data</p>
</div>
)
}
return (
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={chartData}
dataKey="count"
nameKey="type"
innerRadius={60}
strokeWidth={5}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-3xl font-bold"
>
{totalDetections.toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
Total
</tspan>
</text>
)
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
)
}

View File

@@ -1,179 +1,105 @@
"use client"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import * as React from "react"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { Loader2 } from "lucide-react"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
interface LocationData {
name: string
defected_sign_board: number
pothole: number
road_crack: number
damaged_road_marking: number
good_sign_board: number
total: number
name: string
defected_sign_board: number
pothole: number
road_crack: number
damaged_road_marking: number
total: number
}
interface LocationBarChartProps {
data: LocationData[]
isLoading?: boolean
data: LocationData[]
isLoading?: boolean
}
const COLORS = {
defected_sign_board: "#60a5fa", // Lighter Blue
pothole: "#ff8a8a", // Lighter Red
road_crack: "#fbbf24", // Lighter Orange
damaged_road_marking: "#818cf8", // Lighter Indigo
good_sign_board: "#34d399" // Lighter Emerald
}
const chartConfig = {
pothole: {
label: "Potholes",
color: "var(--chart-1)",
},
defected_sign_board: {
label: "Defected Signboards",
color: "var(--chart-2)",
},
road_crack: {
label: "Road Cracks",
color: "var(--chart-3)",
},
damaged_road_marking: {
label: "Damaged Markings",
color: "var(--chart-4)",
},
} satisfies ChartConfig
export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) {
if (isLoading) {
return (
<div className="h-[300px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50" />
</div>
)
}
if (data.length === 0) {
return (
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No location data available</p>
<p className="text-xs mt-1">Process videos to see detections by location</p>
</div>
)
}
if (isLoading) {
return (
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
barCategoryGap="10%"
barGap={2}
>
<defs>
<linearGradient id="barPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ff8a8a" />
<stop offset="100%" stopColor="#ef4444" />
</linearGradient>
<linearGradient id="barDefectedSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" />
<stop offset="100%" stopColor="#3b82f6" />
</linearGradient>
<linearGradient id="barCrack" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbbf24" />
<stop offset="100%" stopColor="#f59e0b" />
</linearGradient>
<linearGradient id="barMarking" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#818cf8" />
<stop offset="100%" stopColor="#6366f1" />
</linearGradient>
<linearGradient id="barGoodSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#34d399" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke="hsl(var(--muted-foreground) / 0.1)"
/>
<XAxis
dataKey="name"
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false}
axisLine={false}
angle={-45}
textAnchor="end"
height={50}
interval={0}
/>
<YAxis
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))' }}
tickLine={false}
axisLine={false}
width={25}
/>
<Tooltip
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg">
<p className="font-medium text-xs mb-1">{label}</p>
{payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-xs">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-muted-foreground">{(entry.name as string).replace(/_/g, ' ')}:</span>
<span className="font-semibold">{entry.value}</span>
</div>
))}
</div>
)
}
return null
}}
/>
<Legend
verticalAlign="top"
height={30}
content={({ payload }) => (
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mb-2">
{payload?.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-1.5">
<div
className="w-2.5 h-2.5 rounded-sm"
style={{ backgroundColor: entry.color }}
/>
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
{(entry.value as string).replace(/_/g, ' ')}
</span>
</div>
))}
</div>
)}
/>
<Bar
dataKey="pothole"
name="Pothole"
fill="url(#barPothole)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="defected_sign_board"
name="Defected Signboard"
fill="url(#barDefectedSign)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="road_crack"
name="Road Crack"
fill="url(#barCrack)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="damaged_road_marking"
name="Damaged Marking"
fill="url(#barMarking)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="good_sign_board"
name="Good Signboard"
fill="url(#barGoodSign)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
</BarChart>
</ResponsiveContainer>
</div>
<div className="h-[250px] flex items-center justify-center">
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
</div>
)
}
if (data.length === 0) {
return (
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
<p className="text-sm">No location data available</p>
<p className="text-xs mt-1">Process videos to see detections by location</p>
</div>
)
}
// Filter out good signboards for a more "damage-focused" standard chart as per multiple bar example
const chartData = data.map(item => ({
name: item.name,
pothole: item.pothole,
defected_sign_board: item.defected_sign_board,
road_crack: item.road_crack,
damaged_road_marking: item.damaged_road_marking,
}))
return (
<div className="h-[250px] w-full">
<ChartContainer config={chartConfig} className="h-full w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} strokeOpacity={0.1} />
<XAxis
dataKey="name"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.length > 8 ? `${value.slice(0, 8)}...` : value}
fontSize={12}
/>
<YAxis
tickLine={false}
axisLine={false}
fontSize={12}
tickMargin={10}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dashed" />}
/>
<Bar dataKey="pothole" fill="var(--color-pothole)" radius={4} />
<Bar dataKey="defected_sign_board" fill="var(--color-defected_sign_board)" radius={4} />
<Bar dataKey="road_crack" fill="var(--color-road_crack)" radius={4} />
<Bar dataKey="damaged_road_marking" fill="var(--color-damaged_road_marking)" radius={4} />
</BarChart>
</ChartContainer>
</div>
)
}

View File

@@ -19,7 +19,7 @@ export function StatsCard({
isLoading = false
}: StatsCardProps) {
return (
<Card className="py-6 px-6">
<Card className="py-6 px-6 hover:scale-105 transition-all duration-300 ease-in-out">
<CardContent className="p-0 flex items-center justify-between gap-6">
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">