diff --git a/src/App.tsx b/src/App.tsx index 8d1c345..4e79662 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { LayoutGroup } from 'motion/react'; import { BrowserRouter, Routes, Route, useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom'; import Header from './components/Header'; import BrowseView from './components/BrowseView'; +import DashboardView from './components/DashboardView'; import DetailView from './components/DetailView'; import CastView from './components/CastView'; import CastDetailView from './components/CastDetailView'; @@ -39,7 +40,30 @@ function AppContent() { // Load media from API on component mount (only when not on cast routes) const [apiMedia, setApiMedia] = useState([]); const [mediaLoading, setMediaLoading] = useState(true); - + + // Map URL paths to categories + const pathToCategory: Record = { + 'anime': 'Anime', + 'movies': 'Movies', + 'tv-series': 'TV Series', + 'music': 'Music', + 'books': 'Books', + 'games': 'Games', + 'consoles': 'Consoles', + 'adult': 'Adult' + }; + + // Set category from URL path on mount or location change + useEffect(() => { + const pathParts = location.pathname.split('/').filter(Boolean); + if (pathParts.length === 1 && pathToCategory[pathParts[0]]) { + const category = pathToCategory[pathParts[0]]; + if (enabledCategories.includes(category)) { + setActiveCategory(category); + } + } + }, [location.pathname, enabledCategories]); + useEffect(() => { const loadSettingsFromApi = async () => { try { @@ -54,7 +78,7 @@ function AppContent() { console.error('Failed to load settings from API:', error); } }; - + loadSettingsFromApi(); }, [setTheme]); @@ -131,8 +155,18 @@ function AppContent() { const handleCategoryChange = (category: MediaCategory) => { setActiveCategory(category); - setSearchParams({ category }); - navigate('/'); + // Map category names to URL-friendly paths + const categoryPaths: Record = { + 'Anime': 'anime', + 'Movies': 'movies', + 'TV Series': 'tv-series', + 'Music': 'music', + 'Books': 'books', + 'Games': 'games', + 'Consoles': 'consoles', + 'Adult': 'adult' + }; + navigate(`/${categoryPaths[category]}`); window.scrollTo({ top: 0, behavior: 'smooth' }); }; @@ -300,7 +334,7 @@ function AppContent() { params.delete('search'); } setSearchParams(params); - navigate('/'); + navigate('/browse'); }; return ( @@ -318,6 +352,13 @@ function AppContent() { 0 ? apiMedia : [...MOCK_MEDIA, ...customMedia, DETAIL_MEDIA].filter(m => enabledCategories.includes(m.category))} + onMediaClick={handleMediaClick} + loading={mediaLoading} + /> + } /> + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> + + } /> void; + loading?: boolean; +} + +export default function DashboardView({ mediaList, onMediaClick, loading = false }: DashboardViewProps) { + // Calculate statistics + const stats = useMemo(() => { + const totalMedia = mediaList.length; + const categories = mediaList.reduce((acc, media) => { + acc[media.category] = (acc[media.category] || 0) + 1; + return acc; + }, {} as Record); + + const totalRating = mediaList.reduce((sum, media) => sum + (media.rating || 0), 0); + const avgRating = totalRating > 0 ? (totalRating / mediaList.filter(m => m.rating).length).toFixed(1) : '0.0'; + + const totalPlaytime = mediaList.reduce((sum, media) => sum + (media.playtime || 0), 0); + const totalPlayCount = mediaList.reduce((sum, media) => sum + (media.playCount || 0), 0); + + return { + totalMedia, + categories, + avgRating, + totalPlaytime, + totalPlayCount + }; + }, [mediaList]); + + // Get recently added media (sorted by some indicator - using index as proxy) + const recentMedia = useMemo(() => { + return [...mediaList].slice(0, 8); + }, [mediaList]); + + // Get top rated media + const topRatedMedia = useMemo(() => { + return [...mediaList] + .filter(m => m.rating && m.rating > 0) + .sort((a, b) => (b.rating || 0) - (a.rating || 0)) + .slice(0, 8); + }, [mediaList]); + + // Get most played media + const mostPlayedMedia = useMemo(() => { + return [...mediaList] + .filter(m => m.playCount && m.playCount > 0) + .sort((a, b) => (b.playCount || 0) - (a.playCount || 0)) + .slice(0, 8); + }, [mediaList]); + + // Category icons mapping + const categoryIcons: Record = { + 'Anime': Tv, + 'Movies': Film, + 'TV Series': Tv, + 'Music': Music, + 'Books': Book, + 'Games': Gamepad2, + 'Consoles': Gamepad2, + 'Adult': Users + }; + + // Category colors + const categoryColors: Record = { + 'Anime': 'bg-purple-500/10 text-purple-500 border-purple-500/20', + 'Movies': 'bg-blue-500/10 text-blue-500 border-blue-500/20', + 'TV Series': 'bg-green-500/10 text-green-500 border-green-500/20', + 'Music': 'bg-pink-500/10 text-pink-500 border-pink-500/20', + 'Books': 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20', + 'Games': 'bg-red-500/10 text-red-500 border-red-500/20', + 'Consoles': 'bg-orange-500/10 text-orange-500 border-orange-500/20', + 'Adult': 'bg-gray-500/10 text-gray-500 border-gray-500/20' + }; + + const formatPlaytime = (minutes: number) => { + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + }; + + if (loading) { + return ; + } + + return ( +
+ {/* Header */} +
+

Dashboard

+

Overview of your media collection

+
+ + {/* Stats Cards */} +
+ +
+ + Total +
+
{stats.totalMedia}
+
Media Items
+
+ + +
+ + Average +
+
{stats.avgRating}
+
Rating
+
+ + +
+ + Total +
+
{stats.totalPlayCount}
+
Play Count
+
+ + +
+ + Total +
+
{formatPlaytime(stats.totalPlaytime)}
+
Playtime
+
+
+ + {/* Category Breakdown */} + +

+ + Category Breakdown +

+
+ {(Object.keys(stats.categories) as MediaCategory[]).map((category) => { + const Icon = categoryIcons[category]; + const count = stats.categories[category] || 0; + const percentage = stats.totalMedia > 0 ? ((count / stats.totalMedia) * 100).toFixed(1) : '0'; + + return ( +
+ +
{category}
+
{count}
+
{percentage}%
+
+ ); + })} +
+
+ + {/* Recent Media */} + {recentMedia.length > 0 && ( + +

+ + Recent Additions +

+
+ {recentMedia.map((media) => ( + + ))} +
+
+ )} + + {/* Top Rated Media */} + {topRatedMedia.length > 0 && ( + +

+ + Top Rated +

+
+ {topRatedMedia.map((media) => ( + + ))} +
+
+ )} + + {/* Most Played Media */} + {mostPlayedMedia.length > 0 && ( + +

+ + Most Played +

+
+ {mostPlayedMedia.map((media) => ( + + ))} +
+
+ )} + + {/* Empty State */} + {mediaList.length === 0 && ( +
+
+ +
+

No media found

+

Start by adding media to your collection

+
+ )} +
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 92b1677..8349b0a 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,7 +1,7 @@ -import { Search, User, X, Plus, Download, Settings } from 'lucide-react'; +import { Search, User, X, Plus, Download, Settings, Menu } from 'lucide-react'; import { cn } from '@/lib/utils'; import React, { useState, useEffect } from 'react'; -import { Link, NavLink } from 'react-router-dom'; +import { Link, NavLink, useLocation } from 'react-router-dom'; import { MediaCategory } from '@/types'; import { useTheme } from '@/contexts/ThemeContext'; @@ -25,7 +25,21 @@ export default function Header({ const [isSearchOpen, setIsSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [scrolled, setScrolled] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const { theme } = useTheme(); + const location = useLocation(); + + // Map category names to URL-friendly paths + const categoryPaths: Record = { + 'Anime': 'anime', + 'Movies': 'movies', + 'TV Series': 'tv-series', + 'Music': 'music', + 'Books': 'books', + 'Games': 'games', + 'Consoles': 'consoles', + 'Adult': 'adult' + }; useEffect(() => { const handleScroll = () => { @@ -80,20 +94,31 @@ export default function Header({ kyoo +