YouTube Video Classifier Platform

This commit is contained in:
Francisco Pessano
2025-07-12 00:37:07 -03:00
committed by GitHub
parent 5c59b2a301
commit ed8ad48310
13 changed files with 803 additions and 15 deletions

View 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>
);
}