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:
30
src/App.tsx
30
src/App.tsx
@@ -6,6 +6,11 @@ import { LoginPage } from './pages/LoginPage'
|
||||
import { RegisterPage } from './pages/RegisterPage'
|
||||
import { AuthCallbackPage } from './pages/AuthCallbackPage'
|
||||
import { DashboardPage } from './pages/DashboardPage'
|
||||
import { SoundsPage } from './pages/SoundsPage'
|
||||
import { PlaylistsPage } from './pages/PlaylistsPage'
|
||||
import { ExtractionsPage } from './pages/ExtractionsPage'
|
||||
import { UsersPage } from './pages/admin/UsersPage'
|
||||
import { SettingsPage } from './pages/admin/SettingsPage'
|
||||
import { Toaster } from './components/ui/sonner'
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
@@ -35,6 +40,31 @@ function AppRoutes() {
|
||||
<DashboardPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/sounds" element={
|
||||
<ProtectedRoute>
|
||||
<SoundsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/playlists" element={
|
||||
<ProtectedRoute>
|
||||
<PlaylistsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/extractions" element={
|
||||
<ProtectedRoute>
|
||||
<ExtractionsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/admin/users" element={
|
||||
<ProtectedRoute>
|
||||
<UsersPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/admin/settings" element={
|
||||
<ProtectedRoute>
|
||||
<SettingsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +1,22 @@
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { ModeToggle } from '../components/ModeToggle'
|
||||
import { SocketStatus } from '../components/SocketStatus'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export function DashboardPage() {
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<nav className="shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
<h1 className="text-xl font-semibold">Soundboard</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Welcome, {user?.name}
|
||||
</span>
|
||||
<ModeToggle />
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/30 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<div className="grid gap-4">
|
||||
<div className="border-2 border-dashed border-muted-foreground/25 rounded-lg h-64 flex items-center justify-center">
|
||||
<p className="text-muted-foreground">Dashboard content coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<div className="space-y-6">
|
||||
<SocketStatus />
|
||||
<div className="border-4 border-dashed border-gray-200 dark:border-gray-700 rounded-lg h-96 flex items-center justify-center">
|
||||
<p className="text-gray-500 dark:text-gray-400">Dashboard content coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
21
src/pages/ExtractionsPage.tsx
Normal file
21
src/pages/ExtractionsPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export function ExtractionsPage() {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Extractions' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Audio Extractions</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Audio extraction management interface coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
21
src/pages/PlaylistsPage.tsx
Normal file
21
src/pages/PlaylistsPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export function PlaylistsPage() {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Playlists' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Playlists</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Playlist management interface coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
21
src/pages/SoundsPage.tsx
Normal file
21
src/pages/SoundsPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export function SoundsPage() {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Sounds' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Sounds</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Sound management interface coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
22
src/pages/admin/SettingsPage.tsx
Normal file
22
src/pages/admin/SettingsPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Admin' },
|
||||
{ label: 'Settings' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">System Settings</h1>
|
||||
<p className="text-muted-foreground">
|
||||
System administration interface coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
22
src/pages/admin/UsersPage.tsx
Normal file
22
src/pages/admin/UsersPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export function UsersPage() {
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Admin' },
|
||||
{ label: 'Users' }
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">User Management</h1>
|
||||
<p className="text-muted-foreground">
|
||||
User administration interface coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user