feat: add new pages and layout components for improved navigation and structure
- Added AppLayout component to standardize page layout with breadcrumb support. - Introduced AppSidebar for navigation with user-specific links and admin options. - Created new pages: SoundsPage, PlaylistsPage, ExtractionsPage, UsersPage, and SettingsPage. - Removed obsolete SocketStatus component and replaced it with SocketBadge for connection status. - Updated DashboardPage to utilize the new layout and sidebar components. - Added NavGroup and NavItem components for better organization of sidebar navigation. - Included SocketBadge to display real-time connection status. - Updated package.json to include vitest and coverage-v8 for testing and coverage reporting.
This commit is contained in:
62
src/components/AppLayout.tsx
Normal file
62
src/components/AppLayout.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { AppSidebar } from './AppSidebar'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb'
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: React.ReactNode
|
||||
breadcrumb?: {
|
||||
items: Array<{
|
||||
label: string
|
||||
href?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export function AppLayout({ children, breadcrumb }: AppLayoutProps) {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{breadcrumb && (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{breadcrumb.items.map((item, index) => (
|
||||
<div key={item.label} className="flex items-center gap-2">
|
||||
<BreadcrumbItem>
|
||||
{item.href && index < breadcrumb.items.length - 1 ? (
|
||||
<BreadcrumbLink href={item.href}>
|
||||
{item.label}
|
||||
</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{index < breadcrumb.items.length - 1 && (
|
||||
<BreadcrumbSeparator />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
{children}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
58
src/components/AppSidebar.tsx
Normal file
58
src/components/AppSidebar.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Home,
|
||||
Music,
|
||||
Users,
|
||||
Settings,
|
||||
Download,
|
||||
PlayCircle
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { NavGroup } from './nav/NavGroup'
|
||||
import { NavItem } from './nav/NavItem'
|
||||
import { UserNav } from './nav/UserNav'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
export function AppSidebar() {
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
<Sidebar variant="sidebar" collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2 px-2 py-2">
|
||||
<Music className="h-6 w-6" />
|
||||
<span className="font-semibold text-lg group-data-[collapsible=icon]:hidden">Soundboard</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<NavGroup label="Application">
|
||||
<NavItem href="/" icon={Home} title="Dashboard" />
|
||||
<NavItem href="/sounds" icon={Music} title="Sounds" />
|
||||
<NavItem href="/playlists" icon={PlayCircle} title="Playlists" />
|
||||
<NavItem href="/extractions" icon={Download} title="Extractions" />
|
||||
</NavGroup>
|
||||
|
||||
{user.role === "admin" && (
|
||||
<NavGroup label="Admin">
|
||||
<NavItem href="/admin/users" icon={Users} title="Users" />
|
||||
<NavItem href="/admin/settings" icon={Settings} title="Settings" />
|
||||
</NavGroup>
|
||||
)}
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<UserNav user={user} logout={logout} />
|
||||
</SidebarFooter>
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
16
src/components/SocketBadge.tsx
Normal file
16
src/components/SocketBadge.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useSocket } from '@/contexts/SocketContext'
|
||||
import { Badge } from './ui/badge'
|
||||
|
||||
export function SocketBadge() {
|
||||
const { isConnected, isReconnecting } = useSocket()
|
||||
|
||||
if (isReconnecting) {
|
||||
return <Badge variant="secondary" className="text-xs">Reconnecting</Badge>
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge variant={isConnected ? 'default' : 'destructive'} className="text-xs">
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useSocket } from '../contexts/SocketContext'
|
||||
import { Badge } from './ui/badge'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
|
||||
|
||||
export function SocketStatus() {
|
||||
const { isConnected, connectionError, isReconnecting } = useSocket()
|
||||
|
||||
const getStatusBadge = () => {
|
||||
if (isReconnecting) {
|
||||
return <Badge variant="secondary">Reconnecting...</Badge>
|
||||
}
|
||||
return (
|
||||
<Badge variant={isConnected ? 'default' : 'destructive'}>
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const getStatusMessage = () => {
|
||||
if (isReconnecting) {
|
||||
return <div className="text-muted-foreground text-sm">Reconnecting with refreshed token...</div>
|
||||
}
|
||||
if (connectionError) {
|
||||
return <div className="text-destructive text-sm">{connectionError}</div>
|
||||
}
|
||||
if (isConnected) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground text-sm">Ready for real-time communication</div>
|
||||
<div className="text-xs text-muted-foreground">🔄 Proactive token refresh active</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
WebSocket Status
|
||||
{getStatusBadge()}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{getStatusMessage()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
24
src/components/nav/NavGroup.tsx
Normal file
24
src/components/nav/NavGroup.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
} from '@/components/ui/sidebar'
|
||||
|
||||
interface NavGroupProps {
|
||||
label?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function NavGroup({ label, children }: NavGroupProps) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
{label && <SidebarGroupLabel>{label}</SidebarGroupLabel>}
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{children}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
34
src/components/nav/NavItem.tsx
Normal file
34
src/components/nav/NavItem.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { Link, useLocation } from 'react-router'
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem
|
||||
} from '@/components/ui/sidebar'
|
||||
|
||||
interface NavItemProps {
|
||||
href: string
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
badge?: string | number
|
||||
}
|
||||
|
||||
export function NavItem({ href, icon: Icon, title, badge }: NavItemProps) {
|
||||
const location = useLocation()
|
||||
const isActive = location.pathname === href
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={isActive} tooltip={title}>
|
||||
<Link to={href}>
|
||||
<Icon />
|
||||
<span>{title}</span>
|
||||
{badge && (
|
||||
<span className="ml-auto text-xs bg-primary/20 text-primary rounded-full px-2 py-0.5">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
81
src/components/nav/UserNav.tsx
Normal file
81
src/components/nav/UserNav.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ChevronsUpDown, LogOut, UserIcon } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '../ui/sidebar'
|
||||
import type { User } from '@/types/auth'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
interface UserNavProps {
|
||||
user: User
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
export function UserNav({ user, logout }: UserNavProps) {
|
||||
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-lg">
|
||||
<AvatarImage src={user.picture ?? ''} alt={user.name ?? ''} />
|
||||
<AvatarFallback className="rounded-lg">SB</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{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"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.picture ?? ''} alt={user.name ?? ''} />
|
||||
<AvatarFallback className="rounded-lg">SB</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/account">
|
||||
<UserIcon />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user