mirror of
https://github.com/FranP-code/classify_saved_videos_yt.git
synced 2025-10-13 00:32:25 +00:00
YouTube Video Classifier Platform
This commit is contained in:
committed by
GitHub
parent
5c59b2a301
commit
ed8ad48310
163
web/src/components/VirtualTable.tsx
Normal file
163
web/src/components/VirtualTable.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { Virtualizer, VirtualizerOptions } from '@tanstack/virtual-core';
|
||||
import type { VideoData } from '../types/video';
|
||||
import { formatDuration, formatDate } from '../utils/csvParser';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface VirtualTableProps {
|
||||
data: VideoData[];
|
||||
}
|
||||
|
||||
export function VirtualTable({ data }: VirtualTableProps) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const [virtualizer, setVirtualizer] = useState<Virtualizer<HTMLDivElement, Element> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parentRef.current) return;
|
||||
|
||||
const virtualizerOptions: VirtualizerOptions<HTMLDivElement, Element> = {
|
||||
count: data.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 120,
|
||||
overscan: 5,
|
||||
};
|
||||
|
||||
const newVirtualizer = new Virtualizer(virtualizerOptions);
|
||||
setVirtualizer(newVirtualizer);
|
||||
|
||||
return () => {
|
||||
newVirtualizer.destroy();
|
||||
};
|
||||
}, [data.length]);
|
||||
|
||||
const items = virtualizer?.getVirtualItems() ?? [];
|
||||
|
||||
if (!virtualizer) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{/* Table Header */}
|
||||
<div className="bg-muted/50 border-b p-4">
|
||||
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-muted-foreground">
|
||||
<div className="col-span-3">Video</div>
|
||||
<div className="col-span-2">Channel</div>
|
||||
<div className="col-span-2">Classification</div>
|
||||
<div className="col-span-1">Language</div>
|
||||
<div className="col-span-1">Duration</div>
|
||||
<div className="col-span-2">Date</div>
|
||||
<div className="col-span-1">Actions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Virtual Container */}
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-[600px] overflow-auto"
|
||||
style={{
|
||||
contain: 'strict',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const video = data[item.index];
|
||||
if (!video) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
data-index={item.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${item.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="border-b p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="grid grid-cols-12 gap-4 items-start">
|
||||
{/* Video Info */}
|
||||
<div className="col-span-3 space-y-1">
|
||||
<h3 className="font-medium text-sm leading-tight line-clamp-2">
|
||||
{video.video_title}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{video.detailed_subtags.split(',').slice(0, 3).map((tag, i) => (
|
||||
<Badge key={i} variant="secondary" className="text-xs">
|
||||
{tag.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Channel */}
|
||||
<div className="col-span-2">
|
||||
<div className="text-sm font-medium">{video.channel_name}</div>
|
||||
<div className="text-xs text-muted-foreground">{video.playlist_name}</div>
|
||||
</div>
|
||||
|
||||
{/* Classification */}
|
||||
<div className="col-span-2">
|
||||
<Badge variant="default" className="text-xs">
|
||||
{video.classification}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Language */}
|
||||
<div className="col-span-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{video.language}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className="col-span-1 text-sm">
|
||||
{formatDuration(video.video_length_seconds)}
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="col-span-2 space-y-1">
|
||||
<div className="text-sm">{formatDate(video.video_date)}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Added: {formatDate(video.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="col-span-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.open(video.video_url, '_blank')}
|
||||
className="text-xs"
|
||||
>
|
||||
Watch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-muted/50 border-t p-4">
|
||||
<div className="text-sm text-muted-foreground text-center">
|
||||
Showing {data.length} videos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user