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 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';
|
||||
@@ -40,6 +41,29 @@ function AppContent() {
|
||||
const [apiMedia, setApiMedia] = useState<Media[]>([]);
|
||||
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(() => {
|
||||
const loadSettingsFromApi = async () => {
|
||||
try {
|
||||
@@ -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<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' });
|
||||
};
|
||||
|
||||
@@ -300,7 +334,7 @@ function AppContent() {
|
||||
params.delete('search');
|
||||
}
|
||||
setSearchParams(params);
|
||||
navigate('/');
|
||||
navigate('/browse');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -318,6 +352,13 @@ function AppContent() {
|
||||
<LayoutGroup>
|
||||
<Routes>
|
||||
<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
|
||||
mediaList={filteredMedia}
|
||||
onMediaClick={handleMediaClick}
|
||||
@@ -328,6 +369,94 @@ function AppContent() {
|
||||
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={
|
||||
<MediaDetailRoute
|
||||
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 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<MediaCategory, string> = {
|
||||
'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({
|
||||
</div>
|
||||
kyoo
|
||||
</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">
|
||||
{enabledCategories.map(cat => (
|
||||
<button
|
||||
<NavLink
|
||||
key={cat}
|
||||
onClick={() => onCategoryChange(cat)}
|
||||
className={cn(
|
||||
to={`/${categoryPaths[cat]}`}
|
||||
className={({ isActive }) => cn(
|
||||
"text-sm font-bold transition-colors uppercase tracking-wider",
|
||||
(transparent && !scrolled) || !transparent
|
||||
? activeCategory === cat ? "text-white" : "text-white/60 hover:text-white"
|
||||
: activeCategory === cat ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||
? isActive ? "text-white" : "text-white/60 hover:text-white"
|
||||
: isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
</NavLink>
|
||||
))}
|
||||
<div className={cn(
|
||||
"w-px h-4 mx-2",
|
||||
@@ -188,6 +213,38 @@ export default function Header({
|
||||
/>
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user