feat(results): add video results skeleton and refresh detection summary UI

This commit is contained in:
2026-06-24 00:36:27 +05:30
parent c1c81687d8
commit 67237982c8
4 changed files with 165 additions and 33 deletions

View File

@@ -0,0 +1,105 @@
'use client';
import { Skeleton } from '@/components/ui/skeleton';
function StatSkeleton() {
return (
<div className="flex flex-col items-center rounded-lg border border-muted bg-muted/30 p-4 text-center">
<Skeleton className="mb-2 size-9 rounded-md" />
<Skeleton className="h-6 w-12" />
<Skeleton className="mt-2 h-3 w-20" />
</div>
);
}
function DetectionLogSkeleton() {
return (
<div className="rounded-md border bg-card p-4">
<div className="mb-3 flex items-start justify-between gap-4">
<div className="space-y-2">
<Skeleton className="h-4 w-36" />
<Skeleton className="h-3 w-20" />
</div>
<div className="space-y-2">
<Skeleton className="h-3 w-12" />
<Skeleton className="h-3 w-10" />
</div>
</div>
<div className="space-y-2">
<Skeleton className="h-3 w-32" />
<Skeleton className="h-3 w-44" />
</div>
</div>
);
}
export function VideoResultsSkeleton() {
return (
<div className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-start gap-3">
<Skeleton className="size-10 rounded-md" />
<div className="min-w-0 space-y-2">
<Skeleton className="h-7 w-60 max-w-full" />
<Skeleton className="h-3 w-80 max-w-full" />
</div>
</div>
<Skeleton className="h-8 w-20" />
</div>
<section className="overflow-hidden rounded-xl border bg-card">
<div className="border-b p-6 pb-4">
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded" />
<div className="space-y-2">
<Skeleton className="h-5 w-44" />
<Skeleton className="h-3 w-64" />
</div>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<Skeleton className="aspect-video w-full rounded-lg border" />
<section className="overflow-hidden rounded-lg border bg-card/80">
<div className="flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-3 w-72 max-w-full" />
</div>
<Skeleton className="h-8 w-48" />
</div>
<div className="grid grid-cols-1 gap-2 p-3 sm:grid-cols-3">
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} className="h-16 w-full rounded-md" />
))}
</div>
</section>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-5 w-36" />
</div>
<div className="h-107.5 rounded-lg border bg-muted/20 p-4">
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, index) => (
<DetectionLogSkeleton key={index} />
))}
</div>
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-2 gap-3 md:grid-cols-5">
{Array.from({ length: 5 }).map((_, index) => (
<StatSkeleton key={index} />
))}
</div>
</div>
</section>
</div>
);
}

View File

@@ -2,13 +2,14 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, Loader2, TrendingUp } from 'lucide-react'; import { ArrowLeft, TrendingUp } from 'lucide-react';
import VideoPlayerSection from '@/components/videoPlayerSection'; import VideoPlayerSection from '@/components/videoPlayerSection';
import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { CompletedVideoResult, DetectionType } from '@/types'; import { CompletedVideoResult, DetectionType } from '@/types';
import { getDetectionModeConfig } from '@/constants/detectionModeConfig'; import { getDetectionModeConfig } from '@/constants/detectionModeConfig';
import { useVideoResultsQuery } from '../hooks/useVideoResults'; import { useVideoResultsQuery } from '../hooks/useVideoResults';
import { VideoResultsSkeleton } from './components/VideoResultsSkeleton';
const inferDetectionType = (data: CompletedVideoResult): DetectionType => { const inferDetectionType = (data: CompletedVideoResult): DetectionType => {
if (data.detection_mode) return data.detection_mode as DetectionType; if (data.detection_mode) return data.detection_mode as DetectionType;
@@ -50,11 +51,7 @@ export default function VideoResultsPage() {
}; };
if (isLoading) { if (isLoading) {
return ( return <VideoResultsSkeleton />;
<div className="min-h-[60vh] flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
} }
if (error) { if (error) {

View File

@@ -53,7 +53,7 @@ export default function AnnotatedVideoPlayer({
) : ( ) : (
<> <>
<Loader2 className="h-7 w-7 animate-spin text-primary" /> <Loader2 className="h-7 w-7 animate-spin text-primary" />
<p className="text-xs font-medium uppercase tracking-widest"> <p className="text-xs">
{isLoading ? 'Loading annotated video...' : 'Preparing video...'} {isLoading ? 'Loading annotated video...' : 'Preparing video...'}
</p> </p>
</> </>

View File

@@ -1,7 +1,8 @@
'use client'; 'use client';
import { MapPin } from 'lucide-react'; import { Activity, AlertTriangle, MapPin, SignpostBig } from 'lucide-react';
import { CompletedVideoResult, DetectionResultLog } from '@/types';
import type { CompletedVideoResult, DetectionResultLog } from '@/types';
interface CurrentDetectionBarProps { interface CurrentDetectionBarProps {
activeLog?: DetectionResultLog; activeLog?: DetectionResultLog;
@@ -17,17 +18,23 @@ export default function CurrentDetectionBar({
{ {
label: 'Potholes', label: 'Potholes',
value: currentCounts.unique_potholes || 0, value: currentCounts.unique_potholes || 0,
icon: AlertTriangle,
className: 'text-orange-500', className: 'text-orange-500',
iconClassName: 'bg-orange-500/10 text-orange-500',
}, },
{ {
label: 'Signboards', label: 'Signboards',
value: currentCounts.unique_signboards || 0, value: currentCounts.unique_signboards || 0,
icon: SignpostBig,
className: 'text-blue-500', className: 'text-blue-500',
iconClassName: 'bg-blue-500/10 text-blue-500',
}, },
{ {
label: 'Total', label: 'Total',
value: currentCounts.total_detections || 0, value: currentCounts.total_detections || 0,
icon: Activity,
className: 'text-green-500', className: 'text-green-500',
iconClassName: 'bg-green-500/10 text-green-500',
}, },
]; ];
const latitude = activeLog?.latitude; const latitude = activeLog?.latitude;
@@ -38,33 +45,56 @@ export default function CurrentDetectionBar({
: undefined; : undefined;
return ( return (
<div className="rounded-lg border bg-card px-4 py-3"> <section className="overflow-hidden rounded-lg border bg-card/80">
{coordinates && ( <div className="flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="mb-3 flex justify-end"> <div>
<div className="flex max-w-full items-center gap-2 rounded-md border bg-muted/30 px-3 py-1.5"> <p className="text-sm font-semibold text-foreground">
<MapPin className="size-3.5 shrink-0 text-muted-foreground" /> Current Detection Summary
<span className="truncate font-mono text-xs font-semibold text-foreground"> </p>
<p className="text-xs text-muted-foreground">
Counts update as the playback timeline moves.
</p>
</div>
{coordinates ? (
<div className="flex max-w-full items-center gap-2 rounded-md bg-muted/40 px-3 py-1.5 text-xs text-muted-foreground">
<MapPin className="size-3.5 shrink-0" />
<span className="shrink-0 font-medium">Location</span>
<span className="truncate font-mono font-semibold text-foreground">
{coordinates} {coordinates}
</span> </span>
</div> </div>
</div> ) : null}
)}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{countItems.map((item) => (
<div
key={item.label}
className="flex items-center justify-between gap-3 rounded-lg border bg-muted/20 px-4 py-2"
>
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
{item.label}
</p>
<p className={`text-2xl font-bold leading-none ${item.className}`}>
{item.value}
</p>
</div>
))}
</div> </div>
</div>
<div className="grid grid-cols-1 gap-2 p-3 sm:grid-cols-3">
{countItems.map((item) => {
const Icon = item.icon;
return (
<div
key={item.label}
className="flex items-center justify-between gap-3 rounded-md bg-muted/25 px-3 py-2.5 ring-1 ring-border/60"
>
<div className="flex min-w-0 items-center gap-3">
<span
className={`flex size-8 shrink-0 items-center justify-center rounded-md ${item.iconClassName}`}
>
<Icon className="size-4" />
</span>
<p className="truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{item.label}
</p>
</div>
<p
className={`text-2xl font-bold leading-none ${item.className}`}
>
{item.value}
</p>
</div>
);
})}
</div>
</section>
); );
} }