CUB-122: Scaffold Control Center React frontend
All checks were successful
Dev Build / build-test (pull_request) Successful in 1m57s
All checks were successful
Dev Build / build-test (pull_request) Successful in 1m57s
This commit is contained in:
48
frontend/src/components/ErrorBoundary.tsx
Normal file
48
frontend/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error('ErrorBoundary caught:', error, info)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-surface-darkest p-4">
|
||||
<div className="max-w-md w-full p-6 rounded-xl border border-danger/30 bg-danger/10 text-center">
|
||||
<h2 className="text-xl font-bold text-danger mb-2">Something went wrong</h2>
|
||||
<p className="text-on-surface-variant text-sm mb-4">
|
||||
{this.state.error?.message || 'An unexpected error occurred.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 rounded-lg bg-primary text-surface-darkest font-medium"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
110
frontend/src/components/Layout.tsx
Normal file
110
frontend/src/components/Layout.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Command, Activity, FolderKanban, Monitor, Settings, Menu, X } from 'lucide-react'
|
||||
|
||||
const navItems = [
|
||||
{ to: '/', icon: Command, label: 'Hub' },
|
||||
{ to: '/logs', icon: Activity, label: 'Logs' },
|
||||
{ to: '/projects', icon: FolderKanban, label: 'Projects' },
|
||||
{ to: '/sessions', icon: Monitor, label: 'Sessions' },
|
||||
{ to: '/settings', icon: Settings, label: 'Settings' },
|
||||
]
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-surface-darkest text-on-surface">
|
||||
{/* Desktop Nav Rail */}
|
||||
<aside
|
||||
className={`hidden md:flex flex-col border-r border-surface-light transition-all duration-200 ${
|
||||
expanded ? 'w-64' : 'w-18'
|
||||
}`}
|
||||
onMouseEnter={() => setExpanded(true)}
|
||||
onMouseLeave={() => setExpanded(false)}
|
||||
>
|
||||
<div className="flex items-center gap-3 px-4 h-16 border-b border-surface-light">
|
||||
<Command size={24} className="text-primary shrink-0" />
|
||||
{expanded && <span className="font-bold text-lg whitespace-nowrap">Control Center</span>}
|
||||
</div>
|
||||
<nav className="flex-1 py-4 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-4 py-3 mx-2 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-on-surface-variant hover:bg-surface-light hover:text-on-surface'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon size={20} className="shrink-0" />
|
||||
{expanded && <span className="whitespace-nowrap">{item.label}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Header + Bottom Nav */}
|
||||
<div className="flex-1 flex flex-col md:ml-0">
|
||||
<header className="md:hidden flex items-center justify-between h-16 px-4 border-b border-surface-light bg-surface-dark">
|
||||
<div className="flex items-center gap-2">
|
||||
<Command size={22} className="text-primary" />
|
||||
<span className="font-bold">Control Center</span>
|
||||
</div>
|
||||
<button onClick={() => setMobileOpen(!mobileOpen)} className="p-2">
|
||||
{mobileOpen ? <X size={22} /> : <Menu size={22} />}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
{mobileOpen && (
|
||||
<div className="md:hidden fixed inset-0 z-50 bg-surface-dark/95 backdrop-blur">
|
||||
<div className="flex flex-col p-4 space-y-2">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-4 py-3 rounded-lg ${
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-on-surface-variant'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main className="flex-1 p-4 md:p-6 overflow-auto">{children}</main>
|
||||
|
||||
{/* Mobile Bottom Nav */}
|
||||
<nav className="md:hidden flex items-center justify-around h-16 border-t border-surface-light bg-surface-dark">
|
||||
{navItems.slice(0, 5).map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`flex flex-col items-center gap-1 p-2 text-xs ${
|
||||
isActive ? 'text-primary' : 'text-on-surface-variant'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user