feat: implement sidebar and random pages for test
This commit is contained in:
53
src/App.tsx
53
src/App.tsx
@@ -1,9 +1,13 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router'
|
import { AppLayout } from '@/components/AppLayout'
|
||||||
|
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { AuthProvider } from '@/contexts/AuthContext'
|
import { AuthProvider } from '@/contexts/AuthContext'
|
||||||
|
import { ActivityPage } from '@/pages/ActivityPage'
|
||||||
|
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
||||||
|
import { DashboardPage } from '@/pages/DashboardPage'
|
||||||
import { LoginPage } from '@/pages/LoginPage'
|
import { LoginPage } from '@/pages/LoginPage'
|
||||||
import { RegisterPage } from '@/pages/RegisterPage'
|
import { RegisterPage } from '@/pages/RegisterPage'
|
||||||
import { DashboardPage } from '@/pages/DashboardPage'
|
import { SettingsPage } from '@/pages/SettingsPage'
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -12,14 +16,49 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
<Route
|
|
||||||
path="/dashboard"
|
{/* Protected routes with layout */}
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardPage />
|
<AppLayout>
|
||||||
|
<DashboardPage />
|
||||||
|
</AppLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/activity"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<ActivityPage />
|
||||||
|
</AppLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<SettingsPage />
|
||||||
|
</AppLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/users"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireAdmin>
|
||||||
|
<AppLayout>
|
||||||
|
<AdminUsersPage />
|
||||||
|
</AppLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
19
src/components/AppLayout.tsx
Normal file
19
src/components/AppLayout.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { type ReactNode } from 'react'
|
||||||
|
import { AppSidebar } from '@/components/AppSidebar'
|
||||||
|
|
||||||
|
interface AppLayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppLayout({ children }: AppLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-background">
|
||||||
|
<AppSidebar />
|
||||||
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
123
src/components/AppSidebar.tsx
Normal file
123
src/components/AppSidebar.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { Link, useLocation } from 'react-router'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarNav,
|
||||||
|
SidebarNavItem
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
import {
|
||||||
|
Home,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
Activity,
|
||||||
|
LogOut
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
const navigationItems = [
|
||||||
|
{
|
||||||
|
title: 'Dashboard',
|
||||||
|
href: '/dashboard',
|
||||||
|
icon: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Activity',
|
||||||
|
href: '/activity',
|
||||||
|
icon: Activity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
href: '/settings',
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const adminNavigationItems = [
|
||||||
|
{
|
||||||
|
title: 'Users',
|
||||||
|
href: '/admin/users',
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function AppSidebar() {
|
||||||
|
const { user, logout } = useAuth()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
const allNavItems = [
|
||||||
|
...navigationItems,
|
||||||
|
...(user.role === 'admin' ? adminNavigationItems : [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar className="w-64 border-r">
|
||||||
|
<SidebarHeader>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold">Soundboard</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">v2.0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarNav>
|
||||||
|
{allNavItems.map((item) => {
|
||||||
|
const Icon = item.icon
|
||||||
|
const isActive = location.pathname === item.href
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link key={item.href} to={item.href}>
|
||||||
|
<SidebarNavItem active={isActive}>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{item.title}
|
||||||
|
</SidebarNavItem>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SidebarNav>
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<SidebarFooter>
|
||||||
|
<div className="flex items-center gap-3 p-3 rounded-md bg-sidebar-accent/50">
|
||||||
|
{user.picture && (
|
||||||
|
<img
|
||||||
|
src={user.picture}
|
||||||
|
alt="Profile"
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{user.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Sign out
|
||||||
|
</Button>
|
||||||
|
</SidebarFooter>
|
||||||
|
</Sidebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
116
src/components/ui/sidebar.tsx
Normal file
116
src/components/ui/sidebar.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const sidebarVariants = cva(
|
||||||
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-sidebar text-sidebar-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border",
|
||||||
|
floating: "border shadow-md",
|
||||||
|
},
|
||||||
|
side: {
|
||||||
|
left: "",
|
||||||
|
right: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
side: "left",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SidebarProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof sidebarVariants> {}
|
||||||
|
|
||||||
|
const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
|
||||||
|
({ className, variant, side, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(sidebarVariants({ variant, side, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Sidebar.displayName = "Sidebar"
|
||||||
|
|
||||||
|
const SidebarHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SidebarHeader.displayName = "SidebarHeader"
|
||||||
|
|
||||||
|
const SidebarContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex-1 overflow-y-auto p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SidebarContent.displayName = "SidebarContent"
|
||||||
|
|
||||||
|
const SidebarFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col gap-2 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SidebarFooter.displayName = "SidebarFooter"
|
||||||
|
|
||||||
|
const SidebarNav = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<nav
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SidebarNav.displayName = "SidebarNav"
|
||||||
|
|
||||||
|
const SidebarNavItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
active?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, active, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
active && "bg-sidebar-accent text-sidebar-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SidebarNavItem.displayName = "SidebarNavItem"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sidebar,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarNav,
|
||||||
|
SidebarNavItem,
|
||||||
|
}
|
||||||
83
src/pages/ActivityPage.tsx
Normal file
83
src/pages/ActivityPage.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function ActivityPage() {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recent Actions</CardTitle>
|
||||||
|
<CardDescription>Your recent activity</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Logged in via Google</span>
|
||||||
|
<span className="text-muted-foreground">2 minutes ago</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Updated profile</span>
|
||||||
|
<span className="text-muted-foreground">1 hour ago</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Changed settings</span>
|
||||||
|
<span className="text-muted-foreground">2 hours ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>System Events</CardTitle>
|
||||||
|
<CardDescription>System-wide activity</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>New user registered</span>
|
||||||
|
<span className="text-muted-foreground">15 minutes ago</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Database backup completed</span>
|
||||||
|
<span className="text-muted-foreground">1 hour ago</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>System update applied</span>
|
||||||
|
<span className="text-muted-foreground">3 hours ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Statistics</CardTitle>
|
||||||
|
<CardDescription>Activity overview</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Total Sessions</span>
|
||||||
|
<span className="font-medium">42</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>This Week</span>
|
||||||
|
<span className="font-medium">12</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Average Duration</span>
|
||||||
|
<span className="font-medium">1.5h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
161
src/pages/AdminUsersPage.tsx
Normal file
161
src/pages/AdminUsersPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
|
||||||
|
export function AdminUsersPage() {
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
// Mock user data - in real app this would come from API
|
||||||
|
const users = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
is_active: true,
|
||||||
|
providers: ['password', 'google'],
|
||||||
|
created_at: '2024-01-15T10:30:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Jane Smith',
|
||||||
|
email: 'jane@example.com',
|
||||||
|
role: 'user',
|
||||||
|
is_active: true,
|
||||||
|
providers: ['github'],
|
||||||
|
created_at: '2024-01-20T14:15:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Bob Wilson',
|
||||||
|
email: 'bob@example.com',
|
||||||
|
role: 'user',
|
||||||
|
is_active: false,
|
||||||
|
providers: ['password'],
|
||||||
|
created_at: '2024-01-25T09:45:00Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (user?.role !== 'admin') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Access Denied</CardTitle>
|
||||||
|
<CardDescription>You don't have permission to access this page.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Users</CardTitle>
|
||||||
|
<CardDescription>All registered users in the system</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{users.map((userData) => (
|
||||||
|
<div
|
||||||
|
key={userData.id}
|
||||||
|
className="flex items-center justify-between p-4 border rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{userData.name.split(' ').map(n => n[0]).join('')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="font-medium">{userData.name}</span>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
userData.role === 'admin'
|
||||||
|
? 'bg-purple-100 text-purple-800'
|
||||||
|
: 'bg-green-100 text-green-800'
|
||||||
|
}`}>
|
||||||
|
{userData.role}
|
||||||
|
</span>
|
||||||
|
{!userData.is_active && (
|
||||||
|
<span className="px-2 py-1 bg-red-100 text-red-800 rounded-full text-xs font-medium">
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">{userData.email}</p>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{userData.providers.map((provider) => (
|
||||||
|
<span
|
||||||
|
key={provider}
|
||||||
|
className="px-1.5 py-0.5 bg-secondary rounded text-xs"
|
||||||
|
>
|
||||||
|
{provider}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={userData.is_active ? "outline" : "default"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{userData.is_active ? 'Disable' : 'Enable'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Total Users</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{users.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Registered users</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Active Users</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{users.filter(u => u.is_active).length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Currently active</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Admins</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{users.filter(u => u.role === 'admin').length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Administrator accounts</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,143 +1,126 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import { useNavigate } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { user, logout } = useAuth()
|
const { user } = useAuth()
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
navigate('/login')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout failed:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="space-y-6">
|
||||||
<header className="bg-white shadow">
|
<div>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||||
<div className="flex justify-between items-center py-6">
|
<p className="text-muted-foreground">Welcome back, {user.name}!</p>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
</div>
|
||||||
<Button onClick={handleLogout} variant="outline">
|
|
||||||
Sign out
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div className="px-4 py-6 sm:px-0">
|
{/* User Profile Card */}
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<Card>
|
||||||
{/* User Profile Card */}
|
<CardHeader>
|
||||||
<Card>
|
<CardTitle>Profile Information</CardTitle>
|
||||||
<CardHeader>
|
<CardDescription>Your account details</CardDescription>
|
||||||
<CardTitle>Profile Information</CardTitle>
|
</CardHeader>
|
||||||
<CardDescription>Your account details</CardDescription>
|
<CardContent className="space-y-2">
|
||||||
</CardHeader>
|
<div className="flex items-center space-x-2">
|
||||||
<CardContent className="space-y-2">
|
{user.picture && (
|
||||||
<div className="flex items-center space-x-2">
|
<img
|
||||||
{user.picture && (
|
src={user.picture}
|
||||||
<img
|
alt="Profile"
|
||||||
src={user.picture}
|
className="w-8 h-8 rounded-full"
|
||||||
alt="Profile"
|
/>
|
||||||
className="w-8 h-8 rounded-full"
|
)}
|
||||||
/>
|
<div>
|
||||||
)}
|
<p className="font-medium">{user.name}</p>
|
||||||
<div>
|
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||||||
<p className="font-medium">{user.name}</p>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{user.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-2">
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
user.role === 'admin'
|
|
||||||
? 'bg-purple-100 text-purple-800'
|
|
||||||
: 'bg-green-100 text-green-800'
|
|
||||||
}`}>
|
|
||||||
{user.role}
|
|
||||||
</span>
|
|
||||||
{user.is_active && (
|
|
||||||
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Authentication Methods Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Authentication Methods</CardTitle>
|
|
||||||
<CardDescription>How you can sign in</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{user.providers.map((provider) => (
|
|
||||||
<div key={provider} className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium capitalize">{provider}</span>
|
|
||||||
<span className="text-xs text-green-600">Connected</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Account Status Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Account Status</CardTitle>
|
|
||||||
<CardDescription>Current account information</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">Status:</span>
|
|
||||||
<span className={`text-sm font-medium ${
|
|
||||||
user.is_active ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}>
|
|
||||||
{user.is_active ? 'Active' : 'Disabled'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">Role:</span>
|
|
||||||
<span className="text-sm font-medium">{user.role}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">User ID:</span>
|
|
||||||
<span className="text-sm font-mono">{user.id}</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Admin Section */}
|
|
||||||
{user.role === 'admin' && (
|
|
||||||
<div className="mt-8">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Admin Panel</CardTitle>
|
|
||||||
<CardDescription>Administrative functions</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-sm text-gray-600 mb-4">
|
|
||||||
You have administrator privileges. You can manage users and system settings.
|
|
||||||
</p>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<Button size="sm">Manage Users</Button>
|
|
||||||
<Button size="sm" variant="outline">System Settings</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="pt-2">
|
||||||
</div>
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
</main>
|
user.role === 'admin'
|
||||||
|
? 'bg-purple-100 text-purple-800'
|
||||||
|
: 'bg-green-100 text-green-800'
|
||||||
|
}`}>
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
{user.is_active && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Authentication Methods Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Authentication Methods</CardTitle>
|
||||||
|
<CardDescription>How you can sign in</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{user.providers.map((provider) => (
|
||||||
|
<div key={provider} className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium capitalize">{provider}</span>
|
||||||
|
<span className="text-xs text-green-600">Connected</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Actions Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Quick Actions</CardTitle>
|
||||||
|
<CardDescription>Common tasks and shortcuts</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<Link to="/settings">
|
||||||
|
<Button variant="outline" className="w-full justify-start">
|
||||||
|
Update Settings
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/activity">
|
||||||
|
<Button variant="outline" className="w-full justify-start">
|
||||||
|
View Activity
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<Link to="/admin/users">
|
||||||
|
<Button variant="outline" className="w-full justify-start">
|
||||||
|
Manage Users
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Section */}
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Admin Panel</CardTitle>
|
||||||
|
<CardDescription>Administrative functions and system overview</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
You have administrator privileges. You can manage users and system settings.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Link to="/admin/users">
|
||||||
|
<Button size="sm">Manage Users</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/settings">
|
||||||
|
<Button size="sm" variant="outline">System Settings</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
165
src/pages/SettingsPage.tsx
Normal file
165
src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
|
||||||
|
export function SettingsPage() {
|
||||||
|
const { user } = useAuth()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegenerateApiToken = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/regenerate-api-token', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
alert(`New API token: ${data.api_token}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to regenerate API token:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Profile Information</CardTitle>
|
||||||
|
<CardDescription>Update your personal information</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Full Name</Label>
|
||||||
|
<Input id="name" defaultValue={user.name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input id="email" defaultValue={user.email} disabled />
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Email cannot be changed directly. Contact support if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleSave} disabled={isLoading}>
|
||||||
|
{isLoading ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Account Information</CardTitle>
|
||||||
|
<CardDescription>View your account details</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>User ID</Label>
|
||||||
|
<Input value={user.id} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Role</Label>
|
||||||
|
<Input value={user.role} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Input value={user.is_active ? 'Active' : 'Disabled'} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Authentication Methods</Label>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{user.providers.map((provider) => (
|
||||||
|
<span
|
||||||
|
key={provider}
|
||||||
|
className="px-2 py-1 bg-secondary rounded-md text-xs font-medium"
|
||||||
|
>
|
||||||
|
{provider}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>API Access</CardTitle>
|
||||||
|
<CardDescription>Manage your API token for programmatic access</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>API Token</Label>
|
||||||
|
<Input
|
||||||
|
value={user.api_token ? `${user.api_token.substring(0, 8)}...` : 'No token'}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Use this token for API authentication. Keep it secure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleRegenerateApiToken} disabled={isLoading} variant="outline">
|
||||||
|
{isLoading ? 'Generating...' : 'Regenerate API Token'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Preferences</CardTitle>
|
||||||
|
<CardDescription>Customize your experience</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Theme</Label>
|
||||||
|
<select className="w-full h-9 px-3 rounded-md border border-input bg-background">
|
||||||
|
<option>Light</option>
|
||||||
|
<option>Dark</option>
|
||||||
|
<option>System</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Language</Label>
|
||||||
|
<select className="w-full h-9 px-3 rounded-md border border-input bg-background">
|
||||||
|
<option>English</option>
|
||||||
|
<option>French</option>
|
||||||
|
<option>Spanish</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleSave} disabled={isLoading}>
|
||||||
|
{isLoading ? 'Saving...' : 'Save Preferences'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ interface User {
|
|||||||
is_active: boolean
|
is_active: boolean
|
||||||
provider: string
|
provider: string
|
||||||
providers: string[]
|
providers: string[]
|
||||||
|
api_token?: string
|
||||||
|
api_token_expires_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthResponse {
|
interface AuthResponse {
|
||||||
@@ -87,10 +89,13 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOAuthProviders(): Promise<Record<string, { name: string; display_name: string }>> {
|
async getOAuthProviders(): Promise<Record<
|
||||||
|
string,
|
||||||
|
{ name: string; display_name: string }
|
||||||
|
> | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/auth/providers`)
|
const response = await fetch(`${API_BASE}/auth/providers`)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to get OAuth providers')
|
throw new Error('Failed to get OAuth providers')
|
||||||
}
|
}
|
||||||
@@ -98,12 +103,8 @@ class AuthService {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return data.providers
|
return data.providers
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Backend not available, using fallback OAuth providers')
|
console.error('getOAuthProviders error: ', error)
|
||||||
// Fallback OAuth providers when backend is not running
|
return null
|
||||||
return {
|
|
||||||
google: { name: 'google', display_name: 'Google' },
|
|
||||||
github: { name: 'github', display_name: 'GitHub' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,4 +125,4 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authService = new AuthService()
|
export const authService = new AuthService()
|
||||||
export type { User }
|
export type { User }
|
||||||
|
|||||||
Reference in New Issue
Block a user