Add dark mode, tooltips, and fix column alignment

This commit is contained in:
Francisco Pessano
2025-07-12 01:28:21 -03:00
committed by GitHub
parent c34044d8a1
commit b318550a29
5 changed files with 246 additions and 93 deletions

View File

@@ -0,0 +1,73 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
return context
}

View File

@@ -0,0 +1,21 @@
import React from 'react'
import { Moon, Sun } from 'lucide-react'
import { Button } from './ui/button'
import { useTheme } from './ThemeProvider'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="outline"
size="icon"
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="relative"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View File

@@ -3,6 +3,8 @@ import { VirtualTable } from './VirtualTable';
import { SearchAndFilter } from './SearchAndFilter';
import { StatsOverview } from './StatsOverview';
import { Button } from './ui/button';
import { ThemeProvider } from './ThemeProvider';
import { ThemeToggle } from './ThemeToggle';
import type { VideoData } from '../types/video';
import { searchVideos, filterVideos } from '../utils/search';
@@ -56,74 +58,79 @@ export default function VideoClassifierApp({ videos }: { videos: VideoData[] })
}
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-6 py-8 space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
YouTube Video Classifier
</h1>
<p className="text-muted-foreground max-w-2xl mx-auto">
AI-powered video classification and management platform. Search, filter, and organize your YouTube video collection with intelligent categorization.
</p>
</div>
{/* Stats Overview */}
{videos.length > 0 && (
<StatsOverview data={videos} />
)}
{/* Search and Filter */}
{videos.length > 0 && (
<SearchAndFilter
data={videos}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
filters={filters}
onFilterChange={setFilters}
totalResults={videos.length}
filteredResults={filteredData.length}
/>
)}
{/* Results */}
{videos.length > 0 ? (
filteredData.length > 0 ? (
<VirtualTable data={filteredData} />
) : (
<div className="text-center py-12">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-lg font-medium mb-2">No results found</h3>
<p className="text-muted-foreground">
Try adjusting your search terms or filters
</p>
<ThemeProvider defaultTheme="system" storageKey="video-classifier-theme">
<div className="min-h-screen bg-background">
<div className="container mx-auto px-6 py-8 space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<div className="flex items-center justify-center gap-4">
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
YouTube Video Classifier
</h1>
<ThemeToggle />
</div>
)
) : (
<div className="text-center py-12">
<div className="text-6xl mb-4">📹</div>
<h3 className="text-lg font-medium mb-2">No videos found</h3>
<p className="text-muted-foreground">
Your CSV file appears to be empty
<p className="text-muted-foreground max-w-2xl mx-auto">
AI-powered video classification and management platform. Search, filter, and organize your YouTube video collection with intelligent categorization.
</p>
</div>
)}
{/* Footer */}
<div className="text-center text-sm text-muted-foreground border-t pt-8">
<p>
{videos.length} videos indexed
</p>
<Button
variant="ghost"
size="sm"
onClick={() => window.location.reload}
className="mt-2"
>
Refresh Data
</Button>
{/* Stats Overview */}
{videos.length > 0 && (
<StatsOverview data={videos} />
)}
{/* Search and Filter */}
{videos.length > 0 && (
<SearchAndFilter
data={videos}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
filters={filters}
onFilterChange={setFilters}
totalResults={videos.length}
filteredResults={filteredData.length}
/>
)}
{/* Results */}
{videos.length > 0 ? (
filteredData.length > 0 ? (
<VirtualTable data={filteredData} />
) : (
<div className="text-center py-12">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-lg font-medium mb-2">No results found</h3>
<p className="text-muted-foreground">
Try adjusting your search terms or filters
</p>
</div>
)
) : (
<div className="text-center py-12">
<div className="text-6xl mb-4">📹</div>
<h3 className="text-lg font-medium mb-2">No videos found</h3>
<p className="text-muted-foreground">
Your CSV file appears to be empty
</p>
</div>
)}
{/* Footer */}
<div className="text-center text-sm text-muted-foreground border-t pt-8">
<p>
{videos.length} videos indexed
</p>
<Button
variant="ghost"
size="sm"
onClick={() => window.location.reload}
className="mt-2"
>
Refresh Data
</Button>
</div>
</div>
</div>
</div>
</ThemeProvider>
);
}

View File

@@ -4,6 +4,7 @@ import type { VideoData } from '../types/video';
import { formatDuration, formatDate } from '../utils/csvParser';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Tooltip } from './ui/tooltip';
interface VirtualTableProps {
data: VideoData[];
@@ -25,15 +26,15 @@ export function VirtualTable({ data }: VirtualTableProps) {
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-2">Video</div>
<div className="col-span-2">Channel</div>
<div className="col-span-2">Classification</div>
<div className="col-span-2">Language</div>
<div className="col-span-1">Duration</div>
<div className="col-span-2">Date</div>
<div className="col-span-1">Actions</div>
<div className="bg-muted/50 border-b">
<div className="grid grid-cols-12 gap-3 px-4 py-3 text-sm font-medium text-muted-foreground">
<div className="col-span-3 flex items-center">Video</div>
<div className="col-span-2 flex items-center">Channel</div>
<div className="col-span-2 flex items-center">Classification</div>
<div className="col-span-1 flex items-center">Language</div>
<div className="col-span-1 flex items-center">Duration</div>
<div className="col-span-2 flex items-center">Date</div>
<div className="col-span-1 flex items-center">Actions</div>
</div>
</div>
@@ -69,51 +70,66 @@ export function VirtualTable({ data }: VirtualTableProps) {
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className="border-b p-4 hover:bg-muted/50 transition-colors">
<div className="grid grid-cols-12 gap-4 items-center">
<div className="border-b hover:bg-muted/50 transition-colors">
<div className="grid grid-cols-12 gap-3 px-4 py-3 items-center min-h-[80px]">
{/* Video Info */}
<div className="col-span-2 space-y-1">
<h3 className="font-medium text-sm leading-tight line-clamp-2">
{video.video_title}
</h3>
<div className="col-span-3 space-y-2 min-w-0">
<Tooltip content={video.video_title}>
<h3 className="font-medium text-sm leading-tight truncate">
{video.video_title}
</h3>
</Tooltip>
<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>
{video.detailed_subtags.split(',').slice(0, 2).map((tag, i) => (
<Tooltip key={i} content={tag.trim()}>
<Badge variant="secondary" className="text-xs max-w-20 truncate">
{tag.trim()}
</Badge>
</Tooltip>
))}
{video.detailed_subtags.split(',').length > 2 && (
<Tooltip content={`+${video.detailed_subtags.split(',').length - 2} more tags`}>
<Badge variant="outline" className="text-xs">
+{video.detailed_subtags.split(',').length - 2}
</Badge>
</Tooltip>
)}
</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 className="col-span-2 min-w-0">
<Tooltip content={video.channel_name}>
<div className="text-sm font-medium truncate">{video.channel_name}</div>
</Tooltip>
<Tooltip content={video.playlist_name}>
<div className="text-xs text-muted-foreground truncate">{video.playlist_name}</div>
</Tooltip>
</div>
{/* Classification */}
<div className="col-span-2">
<Badge variant="default" className="text-xs">
<div className="col-span-2 min-w-0">
<Badge variant="default" className="text-xs truncate max-w-full">
{video.classification}
</Badge>
</div>
{/* Language */}
<div className="col-span-2">
<Badge variant="outline" className="text-xs">
<div className="col-span-1 min-w-0">
<Badge variant="outline" className="text-xs truncate max-w-full">
{video.language}
</Badge>
</div>
{/* Duration */}
<div className="col-span-1 text-sm">
<div className="col-span-1 text-sm font-mono">
{formatDuration(video.video_length_seconds)}
</div>
{/* Date */}
<div className="col-span-2 space-y-1">
<div className="col-span-2 space-y-1 min-w-0">
<div className="text-sm">{formatDate(video.video_date)}</div>
<div className="text-xs text-muted-foreground">
<div className="text-xs text-muted-foreground truncate">
Added: {formatDate(video.timestamp)}
</div>
</div>
@@ -138,7 +154,7 @@ export function VirtualTable({ data }: VirtualTableProps) {
</div>
{/* Footer */}
<div className="bg-muted/50 border-t p-4">
<div className="bg-muted/50 border-t px-4 py-3">
<div className="text-sm text-muted-foreground text-center">
Showing {data.length} videos
</div>

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface TooltipProps {
content: string
children: React.ReactNode
className?: string
}
export function Tooltip({ content, children, className }: TooltipProps) {
const [isVisible, setIsVisible] = React.useState(false)
return (
<div className="relative inline-block">
<div
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
className="cursor-help"
>
{children}
</div>
{isVisible && (
<div className={cn(
"absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 dark:bg-gray-700 rounded-lg shadow-lg",
"bottom-full left-1/2 transform -translate-x-1/2 mb-2",
"max-w-xs break-words",
"before:content-[''] before:absolute before:top-full before:left-1/2 before:transform before:-translate-x-1/2",
"before:border-4 before:border-transparent before:border-t-gray-900 dark:before:border-t-gray-700",
className
)}>
{content}
</div>
)}
</div>
)
}