feat: add My Uploads listing with card and table views

This commit is contained in:
2026-06-18 21:22:44 +05:30
parent dff9e2222f
commit a18fff4d6f
28 changed files with 1060 additions and 127 deletions

View File

@@ -1,134 +1,123 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader2, TrendingUp, UploadCloud } from 'lucide-react';
import { useCallback, useState } from 'react';
import { Plus, UploadCloud } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { videoService } from '@/services/api';
import type { SessionContext } from '@/types';
import { ROUTES } from '@/utils/routes';
import type { UploadVideo } from '@/types';
import { InputDataSection } from './components/InputDataSection';
import { LocationSection } from './components/LocationSection';
import { UploadSettingsSection } from './components/UploadSettingsSection';
import { CreateUploadDialog } from './components/CreateUploadDialog';
import { ProcessedVideoDialog } from './components/ProcessedVideoDialog';
import { UploadCardGrid } from './components/UploadCardGrid';
import { useUploadColumns } from './components/UploadColumns';
import { UploadDetailsDialog } from './components/UploadDetailsDialog';
import { UploadTable } from './components/UploadTable';
import { ViewSwitcher } from './components/ViewSwitcher';
import { useUploadFilters } from './hooks/useUploadFilters';
import { useMyUploadsQuery } from './hooks/useUploadQueries';
export default function UploadPage() {
const router = useRouter();
const [session, setSession] = useState<SessionContext | null>(null);
const [videoFile, setVideoFile] = useState<File | null>(null);
const [gpsFile, setGpsFile] = useState<File | null>(null);
const [analysisMethod, setAnalysisMethod] = useState('yolo');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const isLocationComplete = Boolean(
session?.projectId && session.packageId && session.chainageId,
);
const canSubmit = Boolean(
isLocationComplete && videoFile && gpsFile && analysisMethod,
const { skip, setSkip, limit, setLimit, viewMode, setViewMode } =
useUploadFilters();
const uploadsQuery = useMyUploadsQuery({ skip, limit });
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [detailsUpload, setDetailsUpload] = useState<UploadVideo | null>(null);
const [processedUpload, setProcessedUpload] = useState<UploadVideo | null>(
null,
);
const handleSubmit = async () => {
if (!session?.chainageId || !videoFile || !gpsFile) {
setError('Complete location and input data before upload.');
return;
}
const uploads = uploadsQuery.data?.items ?? [];
const total = uploadsQuery.data?.total ?? 0;
const isLoading = uploadsQuery.isLoading;
const formData = new FormData();
formData.append('file', videoFile);
formData.append('detection_mode', analysisMethod);
formData.append('chainage_id', session.chainageId);
formData.append('json_file', gpsFile);
const handleViewDetails = useCallback((upload: UploadVideo) => {
setDetailsUpload(upload);
}, []);
setIsSubmitting(true);
setError(null);
const handleViewProcessedVideo = useCallback((upload: UploadVideo) => {
setProcessedUpload(upload);
}, []);
try {
await videoService.uploadVideo(formData);
router.push(ROUTES.TICKET);
} catch (err) {
setIsSubmitting(false);
if (err instanceof TypeError && err.message === 'Failed to fetch') {
setError('Cannot connect to server. Please check if backend is running.');
const handleOpenUpload = useCallback(
(upload: UploadVideo) => {
if (upload.processed_video_url) {
handleViewProcessedVideo(upload);
return;
}
setError(err instanceof Error ? err.message : 'Upload failed');
}
};
handleViewDetails(upload);
},
[handleViewDetails, handleViewProcessedVideo],
);
const columns = useUploadColumns({
onViewDetails: handleViewDetails,
onViewProcessedVideo: handleViewProcessedVideo,
});
return (
<div className="space-y-5">
<PageHeader
title="Upload"
description="Configure location, upload data, and submit it for processing."
icon={TrendingUp}
/>
<Card>
<CardContent className="space-y-5 p-5">
<LocationSection
value={session}
onChange={setSession}
disabled={isSubmitting}
/>
<Separator />
<InputDataSection
videoFile={videoFile}
gpsFile={gpsFile}
onVideoFileChange={(file) => {
setVideoFile(file);
setError(null);
}}
onGpsFileChange={(file) => {
setGpsFile(file);
setError(null);
}}
disabled={isSubmitting}
/>
<Separator />
<UploadSettingsSection
value={analysisMethod}
onValueChange={setAnalysisMethod}
disabled={isSubmitting}
/>
{error ? (
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
) : null}
<div className="flex justify-end">
<Button
type="button"
size="lg"
disabled={!canSubmit || isSubmitting}
onClick={handleSubmit}
className="w-full gap-2 md:w-auto"
>
{isSubmitting ? (
<>
<Loader2 className="size-4 animate-spin" />
Uploading...
</>
) : (
<>
<UploadCloud className="size-4" />
Upload
</>
)}
<>
<main className="space-y-5">
<PageHeader
title="My Uploads"
description="View and manage uploaded road inspection videos."
icon={UploadCloud}
actions={
<Button onClick={() => setIsCreateOpen(true)} size="sm">
<Plus className="size-4" />
Upload New
</Button>
</div>
</CardContent>
</Card>
</div>
}
/>
<div className="flex justify-end">
<ViewSwitcher value={viewMode} onValueChange={setViewMode} />
</div>
{viewMode === 'card' ? (
<UploadCardGrid
uploads={uploads}
isLoading={isLoading}
skip={skip}
limit={limit}
total={total}
onPageChange={setSkip}
onLimitChange={setLimit}
onUploadNew={() => setIsCreateOpen(true)}
onOpenUpload={handleOpenUpload}
onViewDetails={handleViewDetails}
onViewProcessedVideo={handleViewProcessedVideo}
/>
) : (
<UploadTable
columns={columns}
uploads={uploads}
isLoading={isLoading}
skip={skip}
limit={limit}
total={total}
onPageChange={setSkip}
onLimitChange={setLimit}
onRowClick={handleOpenUpload}
/>
)}
</main>
<CreateUploadDialog open={isCreateOpen} onOpenChange={setIsCreateOpen} />
<UploadDetailsDialog
upload={detailsUpload}
open={Boolean(detailsUpload)}
onOpenChange={(open) => {
if (!open) setDetailsUpload(null);
}}
/>
<ProcessedVideoDialog
upload={processedUpload}
open={Boolean(processedUpload)}
onOpenChange={(open) => {
if (!open) setProcessedUpload(null);
}}
/>
</>
);
}