Add Dashboard view and routing; mobile header menu
Introduce a new DashboardView component (src/components/DashboardView.tsx) that shows collection stats, recent/top/most-played lists and uses motion + Loading. Wire the dashboard into App (src/App.tsx): import DashboardView, add a root route for /, add per-category routes (/anime, /movies, /tv-series, etc.), map URL paths to MediaCategory, and update navigation/search behavior to use category paths (navigate to /<category>). Update Header (src/components/Header.tsx) to use NavLink for category links, add a mobile menu toggle with a Menu icon, and add URL-friendly category path mapping for consistent navigation.
This commit is contained in:
135
src/App.tsx
135
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 { BrowserRouter, Routes, Route, useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import BrowseView from './components/BrowseView';
|
import BrowseView from './components/BrowseView';
|
||||||
|
import DashboardView from './components/DashboardView';
|
||||||
import DetailView from './components/DetailView';
|
import DetailView from './components/DetailView';
|
||||||
import CastView from './components/CastView';
|
import CastView from './components/CastView';
|
||||||
import CastDetailView from './components/CastDetailView';
|
import CastDetailView from './components/CastDetailView';
|
||||||
@@ -40,6 +41,29 @@ function AppContent() {
|
|||||||
const [apiMedia, setApiMedia] = useState<Media[]>([]);
|
const [apiMedia, setApiMedia] = useState<Media[]>([]);
|
||||||
const [mediaLoading, setMediaLoading] = useState(true);
|
const [mediaLoading, setMediaLoading] = useState(true);
|
||||||
|
|
||||||
|
// Map URL paths to categories
|
||||||
|
const pathToCategory: Record<string, MediaCategory> = {
|
||||||
|
'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(() => {
|
useEffect(() => {
|
||||||
const loadSettingsFromApi = async () => {
|
const loadSettingsFromApi = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -131,8 +155,18 @@ function AppContent() {
|
|||||||
|
|
||||||
const handleCategoryChange = (category: MediaCategory) => {
|
const handleCategoryChange = (category: MediaCategory) => {
|
||||||
setActiveCategory(category);
|
setActiveCategory(category);
|
||||||
setSearchParams({ category });
|
// Map category names to URL-friendly paths
|
||||||
navigate('/');
|
const categoryPaths: Record<MediaCategory, string> = {
|
||||||
|
'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' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,7 +334,7 @@ function AppContent() {
|
|||||||
params.delete('search');
|
params.delete('search');
|
||||||
}
|
}
|
||||||
setSearchParams(params);
|
setSearchParams(params);
|
||||||
navigate('/');
|
navigate('/browse');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -318,6 +352,13 @@ function AppContent() {
|
|||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
|
<DashboardView
|
||||||
|
mediaList={apiMedia.length > 0 ? apiMedia : [...MOCK_MEDIA, ...customMedia, DETAIL_MEDIA].filter(m => enabledCategories.includes(m.category))}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/browse" element={
|
||||||
<BrowseView
|
<BrowseView
|
||||||
mediaList={filteredMedia}
|
mediaList={filteredMedia}
|
||||||
onMediaClick={handleMediaClick}
|
onMediaClick={handleMediaClick}
|
||||||
@@ -328,6 +369,94 @@ function AppContent() {
|
|||||||
loading={mediaLoading}
|
loading={mediaLoading}
|
||||||
/>
|
/>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/anime" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Anime"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/movies" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Movies"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/tv-series" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="TV Series"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/music" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Music"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/books" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Books"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/games" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Games"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/consoles" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Consoles"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/adult" element={
|
||||||
|
<BrowseView
|
||||||
|
mediaList={filteredMedia}
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
activeCategory="Adult"
|
||||||
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
|
gridItemSize={settings?.gridItemSize}
|
||||||
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
|
/>
|
||||||
|
} />
|
||||||
<Route path="/media/:id" element={
|
<Route path="/media/:id" element={
|
||||||
<MediaDetailRoute
|
<MediaDetailRoute
|
||||||
selectedMedia={selectedMedia}
|
selectedMedia={selectedMedia}
|
||||||
|
|||||||
265
src/components/DashboardView.tsx
Normal file
265
src/components/DashboardView.tsx
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { Media, MediaCategory } from '@/types';
|
||||||
|
import MediaCard from './MediaCard';
|
||||||
|
import { Film, Tv, Music, Book, Gamepad2, Users, Star, TrendingUp, Clock, Hash, Play, Award } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import Loading from '@/components/ui/loading';
|
||||||
|
|
||||||
|
interface DashboardViewProps {
|
||||||
|
mediaList: Media[];
|
||||||
|
onMediaClick: (media: Media) => 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<MediaCategory, number>);
|
||||||
|
|
||||||
|
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<MediaCategory, any> = {
|
||||||
|
'Anime': Tv,
|
||||||
|
'Movies': Film,
|
||||||
|
'TV Series': Tv,
|
||||||
|
'Music': Music,
|
||||||
|
'Books': Book,
|
||||||
|
'Games': Gamepad2,
|
||||||
|
'Consoles': Gamepad2,
|
||||||
|
'Adult': Users
|
||||||
|
};
|
||||||
|
|
||||||
|
// Category colors
|
||||||
|
const categoryColors: Record<MediaCategory, string> = {
|
||||||
|
'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 <Loading message="Loading dashboard..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-24 pb-12 px-6 max-w-[1600px] mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-4xl font-black text-foreground mb-2">Dashboard</h1>
|
||||||
|
<p className="text-muted-foreground font-medium">Overview of your media collection</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="bg-muted/50 rounded-xl p-6 border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<Hash className="w-8 h-8 text-[#6d28d9]" />
|
||||||
|
<span className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Total</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-foreground">{stats.totalMedia}</div>
|
||||||
|
<div className="text-sm text-muted-foreground font-medium mt-1">Media Items</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-muted/50 rounded-xl p-6 border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<Star className="w-8 h-8 text-yellow-500" />
|
||||||
|
<span className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Average</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-foreground">{stats.avgRating}</div>
|
||||||
|
<div className="text-sm text-muted-foreground font-medium mt-1">Rating</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="bg-muted/50 rounded-xl p-6 border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<Play className="w-8 h-8 text-green-500" />
|
||||||
|
<span className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Total</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-foreground">{stats.totalPlayCount}</div>
|
||||||
|
<div className="text-sm text-muted-foreground font-medium mt-1">Play Count</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="bg-muted/50 rounded-xl p-6 border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<Clock className="w-8 h-8 text-blue-500" />
|
||||||
|
<span className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Total</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-black text-foreground">{formatPlaytime(stats.totalPlaytime)}</div>
|
||||||
|
<div className="text-sm text-muted-foreground font-medium mt-1">Playtime</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Breakdown */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="bg-muted/50 rounded-xl p-6 border border-border mb-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-black text-foreground mb-4 flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-[#6d28d9]" />
|
||||||
|
Category Breakdown
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-3">
|
||||||
|
{(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 (
|
||||||
|
<div
|
||||||
|
key={category}
|
||||||
|
className={`rounded-lg p-4 border ${categoryColors[category]} flex flex-col items-center justify-center gap-2`}
|
||||||
|
>
|
||||||
|
<Icon className="w-6 h-6" />
|
||||||
|
<div className="text-xs font-bold uppercase tracking-wider">{category}</div>
|
||||||
|
<div className="text-2xl font-black">{count}</div>
|
||||||
|
<div className="text-xs font-medium opacity-75">{percentage}%</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Recent Media */}
|
||||||
|
{recentMedia.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-black text-foreground mb-4 flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5 text-[#6d28d9]" />
|
||||||
|
Recent Additions
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4">
|
||||||
|
{recentMedia.map((media) => (
|
||||||
|
<MediaCard key={media.id} media={media} onClick={onMediaClick} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Top Rated Media */}
|
||||||
|
{topRatedMedia.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.7 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-black text-foreground mb-4 flex items-center gap-2">
|
||||||
|
<Award className="w-5 h-5 text-[#6d28d9]" />
|
||||||
|
Top Rated
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4">
|
||||||
|
{topRatedMedia.map((media) => (
|
||||||
|
<MediaCard key={media.id} media={media} onClick={onMediaClick} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Most Played Media */}
|
||||||
|
{mostPlayedMedia.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-black text-foreground mb-4 flex items-center gap-2">
|
||||||
|
<Play className="w-5 h-5 text-[#6d28d9]" />
|
||||||
|
Most Played
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4">
|
||||||
|
{mostPlayedMedia.map((media) => (
|
||||||
|
<MediaCard key={media.id} media={media} onClick={onMediaClick} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{mediaList.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||||
|
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
|
||||||
|
<Hash size={32} />
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-bold">No media found</p>
|
||||||
|
<p className="text-sm">Start by adding media to your collection</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { cn } from '@/lib/utils';
|
||||||
import React, { useState, useEffect } from 'react';
|
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 { MediaCategory } from '@/types';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
|
|
||||||
@@ -25,7 +25,21 @@ export default function Header({
|
|||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Map category names to URL-friendly paths
|
||||||
|
const categoryPaths: Record<MediaCategory, string> = {
|
||||||
|
'Anime': 'anime',
|
||||||
|
'Movies': 'movies',
|
||||||
|
'TV Series': 'tv-series',
|
||||||
|
'Music': 'music',
|
||||||
|
'Books': 'books',
|
||||||
|
'Games': 'games',
|
||||||
|
'Consoles': 'consoles',
|
||||||
|
'Adult': 'adult'
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@@ -80,20 +94,31 @@ export default function Header({
|
|||||||
</div>
|
</div>
|
||||||
kyoo
|
kyoo
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
|
className={cn(
|
||||||
|
"md:hidden p-2 transition-colors",
|
||||||
|
(transparent && !scrolled) || !transparent
|
||||||
|
? "text-white/90 hover:text-white"
|
||||||
|
: "text-foreground hover:text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Menu size={20} />
|
||||||
|
</button>
|
||||||
<nav className="hidden md:flex items-center gap-6">
|
<nav className="hidden md:flex items-center gap-6">
|
||||||
{enabledCategories.map(cat => (
|
{enabledCategories.map(cat => (
|
||||||
<button
|
<NavLink
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => onCategoryChange(cat)}
|
to={`/${categoryPaths[cat]}`}
|
||||||
className={cn(
|
className={({ isActive }) => cn(
|
||||||
"text-sm font-bold transition-colors uppercase tracking-wider",
|
"text-sm font-bold transition-colors uppercase tracking-wider",
|
||||||
(transparent && !scrolled) || !transparent
|
(transparent && !scrolled) || !transparent
|
||||||
? activeCategory === cat ? "text-white" : "text-white/60 hover:text-white"
|
? isActive ? "text-white" : "text-white/60 hover:text-white"
|
||||||
: activeCategory === cat ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
: isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{cat}
|
{cat}
|
||||||
</button>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-px h-4 mx-2",
|
"w-px h-4 mx-2",
|
||||||
@@ -188,6 +213,38 @@ export default function Header({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
{isMobileMenuOpen && (
|
||||||
|
<div className="md:hidden absolute top-full left-0 right-0 bg-background border-b border-border shadow-lg">
|
||||||
|
<nav className="flex flex-col p-4 gap-2">
|
||||||
|
{enabledCategories.map(cat => (
|
||||||
|
<NavLink
|
||||||
|
key={cat}
|
||||||
|
to={`/${categoryPaths[cat]}`}
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className={({ isActive }) => cn(
|
||||||
|
"text-sm font-bold transition-colors uppercase tracking-wider py-2 px-4 rounded-lg",
|
||||||
|
isActive ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
<div className="w-full h-px bg-border my-2" />
|
||||||
|
<NavLink
|
||||||
|
to="/cast"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className={({ isActive }) => cn(
|
||||||
|
"text-sm font-bold transition-colors uppercase tracking-wider py-2 px-4 rounded-lg",
|
||||||
|
isActive ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
CAST
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user