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).
302 lines
12 KiB
TypeScript
Executable File
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
|
|
}
|