pounce/frontend/src/components/CommandCenterLayout.tsx
yves.gugger 39c7e905e1 refactor: Consistent max-width for Command Center header and content
LAYOUT CHANGES:

1. CommandCenterLayout header now uses max-w-7xl:
   - Header width matches content width exactly
   - Added mx-auto for centering
   - Cleaner, more consistent visual alignment

2. Main content area:
   - max-w-7xl and centering now in layout wrapper
   - Consistent padding: py-6 sm:py-8

3. PageContainer simplified:
   - Removed max-w-7xl mx-auto (now in parent)
   - Just provides space-y-6 for child spacing

4. Header bar improvements:
   - Slightly reduced height: h-16 sm:h-18
   - Better button styling (hover states)
   - Truncation for long titles on mobile
   - Icons slightly larger for better visibility

RESULT:
All Command Center pages now have perfectly aligned
header and content with the same max-width (max-w-7xl).
2025-12-10 16:29:28 +01:00

302 lines
12 KiB
TypeScript
Executable File

'use client'
import { useEffect, useState, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useStore } from '@/lib/store'
import { Sidebar } from './Sidebar'
import { KeyboardShortcutsProvider, useUserShortcuts } from '@/hooks/useKeyboardShortcuts'
import { Bell, Search, X, Command } from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
interface CommandCenterLayoutProps {
children: React.ReactNode
title?: string
subtitle?: string
actions?: React.ReactNode
}
export function CommandCenterLayout({
children,
title,
subtitle,
actions
}: CommandCenterLayoutProps) {
const router = useRouter()
const { isAuthenticated, isLoading, checkAuth, domains } = useStore()
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [notificationsOpen, setNotificationsOpen] = useState(false)
const [searchOpen, setSearchOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [mounted, setMounted] = useState(false)
const authCheckedRef = useRef(false)
// Ensure component is mounted before rendering
useEffect(() => {
setMounted(true)
}, [])
// Load sidebar state from localStorage
useEffect(() => {
if (mounted) {
const saved = localStorage.getItem('sidebar-collapsed')
if (saved) {
setSidebarCollapsed(saved === 'true')
}
}
}, [mounted])
// Check auth only once on mount
useEffect(() => {
if (!authCheckedRef.current) {
authCheckedRef.current = true
checkAuth()
}
}, [checkAuth])
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login')
}
}, [isLoading, isAuthenticated, router])
// Available domains for notifications
const availableDomains = domains?.filter(d => d.is_available) || []
const hasNotifications = availableDomains.length > 0
// Show loading only if we're still checking auth
if (!mounted || isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<div className="w-8 h-8 border-2 border-accent border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-foreground-muted">Loading Command Center...</p>
</div>
</div>
)
}
if (!isAuthenticated) {
return null
}
return (
<KeyboardShortcutsProvider>
<UserShortcutsWrapper />
<div className="min-h-screen bg-background">
{/* Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.02] rounded-full blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.015] rounded-full blur-[100px]" />
</div>
{/* Sidebar */}
<Sidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
/>
{/* Main Content Area */}
<div
className={clsx(
"relative min-h-screen transition-all duration-300",
// Desktop: adjust for sidebar
"lg:ml-[260px]",
sidebarCollapsed && "lg:ml-[72px]",
// Mobile: no margin, just padding for menu button
"ml-0 pt-16 lg:pt-0"
)}
>
{/* Top Bar */}
<header className="sticky top-0 z-30 bg-gradient-to-r from-background/95 via-background/90 to-background/95 backdrop-blur-xl border-b border-border/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 sm:h-18 flex items-center justify-between">
{/* Left: Title */}
<div className="ml-10 lg:ml-0 min-w-0 flex-1">
{title && (
<h1 className="text-xl sm:text-2xl font-semibold tracking-tight text-foreground truncate">{title}</h1>
)}
{subtitle && (
<p className="text-sm text-foreground-muted mt-0.5 hidden sm:block truncate">{subtitle}</p>
)}
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2 sm:gap-3 shrink-0 ml-4">
{/* Quick Search */}
<button
onClick={() => setSearchOpen(true)}
className="hidden md:flex items-center gap-2 h-9 px-3 bg-foreground/5 hover:bg-foreground/8
border border-border/40 rounded-lg text-sm text-foreground-muted
hover:text-foreground transition-all duration-200 hover:border-border/60"
>
<Search className="w-4 h-4" />
<span className="hidden lg:inline">Search</span>
<kbd className="hidden xl:inline-flex items-center h-5 px-1.5 bg-background border border-border/60
rounded text-[10px] text-foreground-subtle font-mono">K</kbd>
</button>
{/* Mobile Search */}
<button
onClick={() => setSearchOpen(true)}
className="md:hidden flex items-center justify-center w-9 h-9 text-foreground-muted
hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
>
<Search className="w-5 h-5" />
</button>
{/* Notifications */}
<div className="relative">
<button
onClick={() => setNotificationsOpen(!notificationsOpen)}
className={clsx(
"relative flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-200",
notificationsOpen
? "bg-foreground/10 text-foreground"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
>
<Bell className="w-5 h-5" />
{hasNotifications && (
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-accent rounded-full">
<span className="absolute inset-0 rounded-full bg-accent animate-ping opacity-50" />
</span>
)}
</button>
{/* Notifications Dropdown */}
{notificationsOpen && (
<div className="absolute right-0 top-full mt-2 w-80 bg-background-secondary border border-border
rounded-xl shadow-2xl overflow-hidden">
<div className="p-4 border-b border-border flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground">Notifications</h3>
<button
onClick={() => setNotificationsOpen(false)}
className="text-foreground-muted hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="max-h-80 overflow-y-auto">
{availableDomains.length > 0 ? (
<div className="p-2">
{availableDomains.slice(0, 5).map((domain) => (
<Link
key={domain.id}
href="/command/watchlist"
onClick={() => setNotificationsOpen(false)}
className="flex items-start gap-3 p-3 hover:bg-foreground/5 rounded-lg transition-colors"
>
<div className="w-8 h-8 bg-accent/10 rounded-lg flex items-center justify-center shrink-0">
<span className="w-2 h-2 bg-accent rounded-full animate-pulse" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{domain.name}</p>
<p className="text-xs text-accent">Available now!</p>
</div>
</Link>
))}
</div>
) : (
<div className="p-8 text-center">
<Bell className="w-8 h-8 text-foreground-subtle mx-auto mb-3" />
<p className="text-sm text-foreground-muted">No notifications</p>
<p className="text-xs text-foreground-subtle mt-1">
We'll notify you when domains become available
</p>
</div>
)}
</div>
</div>
)}
</div>
{/* Keyboard Shortcuts Hint */}
<button
onClick={() => {}}
className="hidden sm:flex items-center gap-1.5 px-2 py-1.5 text-xs text-foreground-subtle hover:text-foreground
bg-foreground/5 rounded-lg border border-border/40 hover:border-border/60 transition-all"
title="Keyboard shortcuts (?)"
>
<Command className="w-3.5 h-3.5" />
<span>?</span>
</button>
{/* Custom Actions */}
{actions}
</div>
</div>
</header>
{/* Page Content */}
<main className="relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
{children}
</div>
</main>
</div>
{/* Quick Search Modal */}
{searchOpen && (
<div
className="fixed inset-0 z-[60] bg-background/80 backdrop-blur-sm flex items-start justify-center pt-[15vh] sm:pt-[20vh] px-4"
onClick={() => setSearchOpen(false)}
>
<div
className="w-full max-w-xl bg-background-secondary border border-border rounded-2xl shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 p-4 border-b border-border">
<Search className="w-5 h-5 text-foreground-muted" />
<input
type="text"
placeholder="Search domains, TLDs, auctions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1 bg-transparent text-foreground placeholder:text-foreground-subtle
outline-none text-lg"
autoFocus
/>
<button
onClick={() => setSearchOpen(false)}
className="flex items-center h-6 px-2 bg-background border border-border
rounded text-xs text-foreground-subtle font-mono hover:text-foreground transition-colors"
>
ESC
</button>
</div>
<div className="p-6 text-center text-foreground-muted text-sm">
Start typing to search...
</div>
</div>
</div>
)}
{/* Keyboard shortcut for search */}
<KeyboardShortcut onTrigger={() => setSearchOpen(true)} keys={['Meta', 'k']} />
</div>
</KeyboardShortcutsProvider>
)
}
// Keyboard shortcut component
function KeyboardShortcut({ onTrigger, keys }: { onTrigger: () => void, keys: string[] }) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (keys.includes('Meta') && e.metaKey && e.key === 'k') {
e.preventDefault()
onTrigger()
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [onTrigger, keys])
return null
}
// User shortcuts wrapper
function UserShortcutsWrapper() {
useUserShortcuts()
return null
}