feat: add layout and sidebar components with navigation structure
chore: update dependencies and configuration for Tailwind CSS docs: create components.json and skills-lock.json for project structure
This commit is contained in:
@@ -1,119 +1,309 @@
|
||||
import { Link, useLocation } from '@tanstack/react-router';
|
||||
import { Home, Gamepad2, FileText, Settings, Database, Tag, Download, Upload } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
'use client';
|
||||
|
||||
interface SidebarProps {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
import * as React from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import {
|
||||
Gamepad2,
|
||||
Home,
|
||||
Settings,
|
||||
User,
|
||||
Database,
|
||||
Star,
|
||||
TrendingUp,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
} from 'lucide-react';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home, count: null },
|
||||
{ name: 'Juegos', href: '/games', icon: Gamepad2, count: null },
|
||||
{
|
||||
name: 'Plataformas',
|
||||
href: '/platforms',
|
||||
icon: Database,
|
||||
count: null,
|
||||
const data = {
|
||||
user: {
|
||||
name: 'Game Library',
|
||||
email: 'admin@gamelibrary.com',
|
||||
avatar: '/avatars/default.jpg',
|
||||
},
|
||||
{
|
||||
name: 'Etiquetas',
|
||||
href: '/tags',
|
||||
icon: Tag,
|
||||
count: null,
|
||||
},
|
||||
{ name: 'Importar ROMs', href: '/import', icon: Download, count: null },
|
||||
{ name: 'Exportar', href: '/export', icon: Upload, count: null },
|
||||
{ name: 'Configuración', href: '/settings', icon: Settings, count: null },
|
||||
];
|
||||
teams: [
|
||||
{
|
||||
name: 'Personal',
|
||||
logo: Gamepad2,
|
||||
plan: 'Standard',
|
||||
},
|
||||
{
|
||||
name: 'Work',
|
||||
logo: Database,
|
||||
plan: 'Professional',
|
||||
},
|
||||
],
|
||||
navMain: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/',
|
||||
icon: Home,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: 'Games',
|
||||
url: '/games',
|
||||
icon: Gamepad2,
|
||||
items: [
|
||||
{
|
||||
title: 'All Games',
|
||||
url: '/games',
|
||||
},
|
||||
{
|
||||
title: 'Favorites',
|
||||
url: '/games/favorites',
|
||||
},
|
||||
{
|
||||
title: 'Recently Played',
|
||||
url: '/games/recent',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Collections',
|
||||
url: '/collections',
|
||||
icon: Star,
|
||||
items: [
|
||||
{
|
||||
title: 'My Collections',
|
||||
url: '/collections',
|
||||
},
|
||||
{
|
||||
title: 'Shared Collections',
|
||||
url: '/collections/shared',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Statistics',
|
||||
url: '/stats',
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
url: '/settings',
|
||||
icon: Settings,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
function TeamSwitcher({
|
||||
teams,
|
||||
}: {
|
||||
teams: {
|
||||
name: string;
|
||||
logo: React.ElementType;
|
||||
plan: string;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
|
||||
|
||||
if (!activeTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay para móviles */}
|
||||
{isOpen && <div className="fixed inset-0 z-40 bg-black/50 md:hidden" onClick={onClose} />}
|
||||
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-50 h-full w-64 transform border-r bg-background transition-transform duration-300 ease-in-out md:relative md:translate-x-0',
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header del sidebar */}
|
||||
<div className="flex h-14 items-center border-b px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Gamepad2 className="h-6 w-6" />
|
||||
<span className="font-bold">Quasar</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="ml-auto md:hidden" onClick={onClose}>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Contenido del sidebar */}
|
||||
<ScrollArea className="flex-1">
|
||||
<nav className="p-4 space-y-1">
|
||||
{navigation.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = location.pathname === item.href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
|
||||
isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
{item.count !== null && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{item.count}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Sección adicional con información */}
|
||||
<div className="mt-8 p-4 border-t">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
Información
|
||||
</h3>
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<div className="flex justify-between">
|
||||
<span>Versión</span>
|
||||
<span>1.0.0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Última actualización</span>
|
||||
<span>2024-01-15</span>
|
||||
</div>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<activeTeam.logo className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer del sidebar */}
|
||||
<div className="flex h-14 items-center border-t px-4">
|
||||
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
||||
<span>© 2024 Quasar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{activeTeam.name}</span>
|
||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
||||
{teams.map((team) => (
|
||||
<DropdownMenuItem
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg mr-2">
|
||||
<team.logo className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{team.name}</span>
|
||||
<span className="truncate text-xs">{team.plan}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: React.ElementType;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Main Navigation</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible key={item.title} defaultOpen={item.isActive} className="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger render={<SidebarMenuButton tooltip={item.title} />}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton render={<a href={subItem.url} />}>
|
||||
<span>{subItem.title}</span>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function NavUser({ user }: { user: { name: string; email: string; avatar: string } }) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-full">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="start"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Logged in as
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<span>Log out</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background px-4 transition-[width,height] ease-linear group-has-[collapsible=icon]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Gamepad2 className="h-6 w-6" />
|
||||
<h1 className="text-lg font-semibold">Game Library</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user