From b318550a29a5f14bf900ba388976767021bda929 Mon Sep 17 00:00:00 2001 From: Francisco Pessano <76450203+FranP-code@users.noreply.github.com> Date: Sat, 12 Jul 2025 01:28:21 -0300 Subject: [PATCH] Add dark mode, tooltips, and fix column alignment --- web/src/components/ThemeProvider.tsx | 73 ++++++++++++ web/src/components/ThemeToggle.tsx | 21 ++++ web/src/components/VideoClassifierApp.tsx | 133 ++++++++++++---------- web/src/components/VirtualTable.tsx | 76 ++++++++----- web/src/components/ui/tooltip.tsx | 36 ++++++ 5 files changed, 246 insertions(+), 93 deletions(-) create mode 100644 web/src/components/ThemeProvider.tsx create mode 100644 web/src/components/ThemeToggle.tsx create mode 100644 web/src/components/ui/tooltip.tsx diff --git a/web/src/components/ThemeProvider.tsx b/web/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..f053414 --- /dev/null +++ b/web/src/components/ThemeProvider.tsx @@ -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(initialState) + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (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 ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + + if (context === undefined) + throw new Error('useTheme must be used within a ThemeProvider') + + return context +} \ No newline at end of file diff --git a/web/src/components/ThemeToggle.tsx b/web/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..0b6d2fd --- /dev/null +++ b/web/src/components/ThemeToggle.tsx @@ -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 ( + + ) +} \ No newline at end of file diff --git a/web/src/components/VideoClassifierApp.tsx b/web/src/components/VideoClassifierApp.tsx index 8547773..8893515 100644 --- a/web/src/components/VideoClassifierApp.tsx +++ b/web/src/components/VideoClassifierApp.tsx @@ -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 ( -
-
- {/* Header */} -
-

- YouTube Video Classifier -

-

- AI-powered video classification and management platform. Search, filter, and organize your YouTube video collection with intelligent categorization. -

-
- - {/* Stats Overview */} - {videos.length > 0 && ( - - )} - - {/* Search and Filter */} - {videos.length > 0 && ( - - )} - - {/* Results */} - {videos.length > 0 ? ( - filteredData.length > 0 ? ( - - ) : ( -
-
🔍
-

No results found

-

- Try adjusting your search terms or filters -

+ +
+
+ {/* Header */} +
+
+

+ YouTube Video Classifier +

+
- ) - ) : ( -
-
📹
-

No videos found

-

- Your CSV file appears to be empty +

+ AI-powered video classification and management platform. Search, filter, and organize your YouTube video collection with intelligent categorization.

- )} - {/* Footer */} -
-

- {videos.length} videos indexed -

- + {/* Stats Overview */} + {videos.length > 0 && ( + + )} + + {/* Search and Filter */} + {videos.length > 0 && ( + + )} + + {/* Results */} + {videos.length > 0 ? ( + filteredData.length > 0 ? ( + + ) : ( +
+
🔍
+

No results found

+

+ Try adjusting your search terms or filters +

+
+ ) + ) : ( +
+
📹
+

No videos found

+

+ Your CSV file appears to be empty +

+
+ )} + + {/* Footer */} +
+

+ {videos.length} videos indexed +

+ +
-
+ ); } \ No newline at end of file diff --git a/web/src/components/VirtualTable.tsx b/web/src/components/VirtualTable.tsx index 946364e..034f7a7 100644 --- a/web/src/components/VirtualTable.tsx +++ b/web/src/components/VirtualTable.tsx @@ -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 (
{/* Table Header */} -
-
-
Video
-
Channel
-
Classification
-
Language
-
Duration
-
Date
-
Actions
+
+
+
Video
+
Channel
+
Classification
+
Language
+
Duration
+
Date
+
Actions
@@ -69,51 +70,66 @@ export function VirtualTable({ data }: VirtualTableProps) { transform: `translateY(${virtualItem.start}px)`, }} > -
-
+
+
{/* Video Info */} -
-

- {video.video_title} -

+
+ +

+ {video.video_title} +

+
- {video.detailed_subtags.split(',').slice(0, 3).map((tag, i) => ( - - {tag.trim()} - + {video.detailed_subtags.split(',').slice(0, 2).map((tag, i) => ( + + + {tag.trim()} + + ))} + {video.detailed_subtags.split(',').length > 2 && ( + + + +{video.detailed_subtags.split(',').length - 2} + + + )}
{/* Channel */} -
-
{video.channel_name}
-
{video.playlist_name}
+
+ +
{video.channel_name}
+
+ +
{video.playlist_name}
+
{/* Classification */} -
- +
+ {video.classification}
{/* Language */} -
- +
+ {video.language}
{/* Duration */} -
+
{formatDuration(video.video_length_seconds)}
{/* Date */} -
+
{formatDate(video.video_date)}
-
+
Added: {formatDate(video.timestamp)}
@@ -138,7 +154,7 @@ export function VirtualTable({ data }: VirtualTableProps) {
{/* Footer */} -
+
Showing {data.length} videos
diff --git a/web/src/components/ui/tooltip.tsx b/web/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..130621d --- /dev/null +++ b/web/src/components/ui/tooltip.tsx @@ -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 ( +
+
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + className="cursor-help" + > + {children} +
+ {isVisible && ( +
+ {content} +
+ )} +
+ ) +} \ No newline at end of file