feat: enhance layout and sidebar functionality with new PageHeader and SidebarContext
This commit is contained in:
15
src/App.tsx
15
src/App.tsx
@@ -1,6 +1,7 @@
|
|||||||
import { AppLayout } from '@/components/AppLayout'
|
import { AppLayout } from '@/components/AppLayout'
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { AuthProvider } from '@/contexts/AuthContext'
|
import { AuthProvider } from '@/contexts/AuthContext'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { ActivityPage } from '@/pages/ActivityPage'
|
import { ActivityPage } from '@/pages/ActivityPage'
|
||||||
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
||||||
import { DashboardPage } from '@/pages/DashboardPage'
|
import { DashboardPage } from '@/pages/DashboardPage'
|
||||||
@@ -22,7 +23,7 @@ function App() {
|
|||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppLayout>
|
<AppLayout title="Dashboard" description="Welcome to your dashboard">
|
||||||
<DashboardPage />
|
<DashboardPage />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
@@ -32,7 +33,7 @@ function App() {
|
|||||||
path="/activity"
|
path="/activity"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppLayout>
|
<AppLayout title="Activity" description="View recent activity and logs">
|
||||||
<ActivityPage />
|
<ActivityPage />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
@@ -42,7 +43,7 @@ function App() {
|
|||||||
path="/settings"
|
path="/settings"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppLayout>
|
<AppLayout title="Settings" description="Manage your account settings and preferences">
|
||||||
<SettingsPage />
|
<SettingsPage />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
@@ -52,7 +53,13 @@ function App() {
|
|||||||
path="/admin/users"
|
path="/admin/users"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute requireAdmin>
|
<ProtectedRoute requireAdmin>
|
||||||
<AppLayout>
|
<AppLayout
|
||||||
|
title="User Management"
|
||||||
|
description="Manage users and their permissions"
|
||||||
|
headerActions={
|
||||||
|
<Button>Add User</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<AdminUsersPage />
|
<AdminUsersPage />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import { type ReactNode } from 'react'
|
import { type ReactNode } from 'react'
|
||||||
import { AppSidebar } from '@/components/AppSidebar'
|
import { AppSidebar } from '@/components/AppSidebar'
|
||||||
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
|
import { SidebarProvider } from '@/contexts/SidebarContext'
|
||||||
|
|
||||||
interface AppLayoutProps {
|
interface AppLayoutProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
headerActions?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppLayout({ children }: AppLayoutProps) {
|
export function AppLayout({ children, title, description, headerActions }: AppLayoutProps) {
|
||||||
return (
|
return (
|
||||||
|
<SidebarProvider>
|
||||||
<div className="flex h-screen bg-background">
|
<div className="flex h-screen bg-background">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<PageHeader title={title} description={description}>
|
||||||
|
{headerActions}
|
||||||
|
</PageHeader>
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto">
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
SidebarNavItem
|
SidebarNavItem
|
||||||
} from '@/components/ui/sidebar'
|
} from '@/components/ui/sidebar'
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
import { useSidebar } from '@/contexts/SidebarContext'
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -45,6 +46,7 @@ const adminNavigationItems = [
|
|||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
|
const { isOpen } = useSidebar()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@@ -63,16 +65,18 @@ export function AppSidebar() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar className="w-64 border-r">
|
<Sidebar className={`border-r transition-all duration-300 ${isOpen ? 'w-64' : 'w-16'}`}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||||
<span className="text-primary-foreground font-bold text-sm">SB</span>
|
<span className="text-primary-foreground font-bold text-sm">SB</span>
|
||||||
</div>
|
</div>
|
||||||
|
{isOpen && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">Soundboard</h2>
|
<h2 className="text-lg font-semibold">Soundboard</h2>
|
||||||
<p className="text-xs text-muted-foreground">v2.0</p>
|
<p className="text-xs text-muted-foreground">v2.0</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
@@ -84,9 +88,12 @@ export function AppSidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={item.href} to={item.href}>
|
<Link key={item.href} to={item.href}>
|
||||||
<SidebarNavItem active={isActive}>
|
<SidebarNavItem
|
||||||
|
active={isActive}
|
||||||
|
title={!isOpen ? item.title : undefined}
|
||||||
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{item.title}
|
{isOpen && item.title}
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -103,19 +110,21 @@ export function AppSidebar() {
|
|||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isOpen && (
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium truncate">{user.name}</p>
|
<p className="text-sm font-medium truncate">{user.name}</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
|
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full justify-start"
|
className={`w-full ${isOpen ? 'justify-start' : 'justify-center'}`}
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4 mr-2" />
|
<LogOut className="h-4 w-4" />
|
||||||
Sign out
|
{isOpen && <span className="ml-2">Sign out</span>}
|
||||||
</Button>
|
</Button>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
42
src/components/PageHeader.tsx
Normal file
42
src/components/PageHeader.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useSidebar } from '@/contexts/SidebarContext'
|
||||||
|
import { Menu } from 'lucide-react'
|
||||||
|
|
||||||
|
interface PageHeaderProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageHeader({ title, description, children }: PageHeaderProps) {
|
||||||
|
const { toggle } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="flex h-14 items-center gap-4 px-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggle}
|
||||||
|
className="h-9 w-9"
|
||||||
|
>
|
||||||
|
<Menu className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Toggle sidebar</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-lg font-semibold">{title}</h1>
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
src/contexts/SidebarContext.tsx
Normal file
35
src/contexts/SidebarContext.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createContext, useContext, useState, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface SidebarContextType {
|
||||||
|
isOpen: boolean
|
||||||
|
toggle: () => void
|
||||||
|
open: () => void
|
||||||
|
close: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarContext = createContext<SidebarContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(true)
|
||||||
|
|
||||||
|
const toggle = () => setIsOpen(prev => !prev)
|
||||||
|
const open = () => setIsOpen(true)
|
||||||
|
const close = () => setIsOpen(false)
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
isOpen,
|
||||||
|
toggle,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebar() {
|
||||||
|
const context = useContext(SidebarContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useSidebar must be used within a SidebarProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -3,11 +3,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
export function ActivityPage() {
|
export function ActivityPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Activity</h1>
|
|
||||||
<p className="text-muted-foreground">View recent activity and logs</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -51,14 +51,6 @@ export function AdminUsersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">User Management</h1>
|
|
||||||
<p className="text-muted-foreground">Manage users and their permissions</p>
|
|
||||||
</div>
|
|
||||||
<Button>Add User</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Users</CardTitle>
|
<CardTitle>Users</CardTitle>
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ export function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
|
||||||
<p className="text-muted-foreground">Welcome back, {user.name}!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{/* User Profile Card */}
|
{/* User Profile Card */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export function LoginPage() {
|
|||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
try {
|
try {
|
||||||
const providers = await authService.getOAuthProviders()
|
const providers = await authService.getOAuthProviders()
|
||||||
|
if (providers) {
|
||||||
setOauthProviders(providers)
|
setOauthProviders(providers)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load OAuth providers:', error)
|
console.error('Failed to load OAuth providers:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export function RegisterPage() {
|
|||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
try {
|
try {
|
||||||
const providers = await authService.getOAuthProviders()
|
const providers = await authService.getOAuthProviders()
|
||||||
|
if (providers) {
|
||||||
setOauthProviders(providers)
|
setOauthProviders(providers)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load OAuth providers:', error)
|
console.error('Failed to load OAuth providers:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,6 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Settings</h1>
|
|
||||||
<p className="text-muted-foreground">Manage your account settings and preferences</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user