USER BACKEND: - Created PremiumTable component with elegant gradient styling - All pages now use consistent max-w-7xl width via PageContainer - Auctions page integrated into CommandCenterLayout with full functionality - Intelligence page updated with PremiumTable and StatCards - Added keyboard shortcuts system (press ? to show help): - G: Dashboard, W: Watchlist, P: Portfolio, A: Auctions - I: Intelligence, S: Settings, N: Add domain, Cmd+K: Search ADMIN BACKEND: - Created separate AdminLayout with dedicated sidebar (red theme) - Admin sidebar with navigation tabs and shortcut hints - Integrated keyboard shortcuts for admin: - O: Overview, U: Users, B: Blog, Y: System, D: Back to dashboard - All tables use consistent PremiumTable component - Professional stat cards and status badges COMPONENTS: - PremiumTable: Elegant table with sorting, selection, loading states - Badge: Versatile status badge with variants and dot indicator - StatCard: Consistent stat display with optional accent styling - PageContainer: Enforces max-w-7xl for consistent page width - TableActionButton: Consistent action buttons for tables - PlatformBadge: Color-coded platform indicators for auctions
312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useCallback, useState, createContext, useContext, ReactNode } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { X, Command, Search } from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
interface Shortcut {
|
|
key: string
|
|
label: string
|
|
description: string
|
|
action: () => void
|
|
category: 'navigation' | 'actions' | 'global'
|
|
requiresModifier?: boolean
|
|
}
|
|
|
|
interface KeyboardShortcutsContextType {
|
|
shortcuts: Shortcut[]
|
|
registerShortcut: (shortcut: Shortcut) => void
|
|
unregisterShortcut: (key: string) => void
|
|
showHelp: boolean
|
|
setShowHelp: (show: boolean) => void
|
|
}
|
|
|
|
// ============================================================================
|
|
// CONTEXT
|
|
// ============================================================================
|
|
|
|
const KeyboardShortcutsContext = createContext<KeyboardShortcutsContextType | null>(null)
|
|
|
|
export function useKeyboardShortcuts() {
|
|
const context = useContext(KeyboardShortcutsContext)
|
|
if (!context) {
|
|
throw new Error('useKeyboardShortcuts must be used within KeyboardShortcutsProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
// ============================================================================
|
|
// PROVIDER
|
|
// ============================================================================
|
|
|
|
export function KeyboardShortcutsProvider({
|
|
children,
|
|
shortcuts: defaultShortcuts = [],
|
|
}: {
|
|
children: ReactNode
|
|
shortcuts?: Shortcut[]
|
|
}) {
|
|
const router = useRouter()
|
|
const [shortcuts, setShortcuts] = useState<Shortcut[]>(defaultShortcuts)
|
|
const [showHelp, setShowHelp] = useState(false)
|
|
|
|
const registerShortcut = useCallback((shortcut: Shortcut) => {
|
|
setShortcuts(prev => {
|
|
const existing = prev.find(s => s.key === shortcut.key)
|
|
if (existing) return prev
|
|
return [...prev, shortcut]
|
|
})
|
|
}, [])
|
|
|
|
const unregisterShortcut = useCallback((key: string) => {
|
|
setShortcuts(prev => prev.filter(s => s.key !== key))
|
|
}, [])
|
|
|
|
// Handle keyboard events
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
// Ignore if user is typing in an input
|
|
if (
|
|
e.target instanceof HTMLInputElement ||
|
|
e.target instanceof HTMLTextAreaElement ||
|
|
e.target instanceof HTMLSelectElement ||
|
|
(e.target as HTMLElement)?.isContentEditable
|
|
) {
|
|
return
|
|
}
|
|
|
|
// Show help with ?
|
|
if (e.key === '?' && !e.metaKey && !e.ctrlKey) {
|
|
e.preventDefault()
|
|
setShowHelp(true)
|
|
return
|
|
}
|
|
|
|
// Close help with Escape
|
|
if (e.key === 'Escape' && showHelp) {
|
|
e.preventDefault()
|
|
setShowHelp(false)
|
|
return
|
|
}
|
|
|
|
// Find matching shortcut
|
|
const shortcut = shortcuts.find(s => {
|
|
if (s.requiresModifier) {
|
|
return (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === s.key.toLowerCase()
|
|
}
|
|
return e.key.toLowerCase() === s.key.toLowerCase() && !e.metaKey && !e.ctrlKey
|
|
})
|
|
|
|
if (shortcut) {
|
|
e.preventDefault()
|
|
shortcut.action()
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
}, [shortcuts, showHelp])
|
|
|
|
return (
|
|
<KeyboardShortcutsContext.Provider value={{ shortcuts, registerShortcut, unregisterShortcut, showHelp, setShowHelp }}>
|
|
{children}
|
|
{showHelp && <ShortcutsModal shortcuts={shortcuts} onClose={() => setShowHelp(false)} />}
|
|
</KeyboardShortcutsContext.Provider>
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// SHORTCUTS MODAL
|
|
// ============================================================================
|
|
|
|
function ShortcutsModal({ shortcuts, onClose }: { shortcuts: Shortcut[]; onClose: () => void }) {
|
|
const categories = {
|
|
navigation: shortcuts.filter(s => s.category === 'navigation'),
|
|
actions: shortcuts.filter(s => s.category === 'actions'),
|
|
global: shortcuts.filter(s => s.category === 'global'),
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-[100] bg-background/80 backdrop-blur-sm flex items-center justify-center p-4"
|
|
onClick={onClose}
|
|
>
|
|
<div
|
|
className="w-full max-w-lg bg-background border border-border/50 rounded-2xl shadow-2xl overflow-hidden"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-background-secondary/50">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 bg-accent/10 rounded-xl flex items-center justify-center">
|
|
<Command className="w-4 h-4 text-accent" />
|
|
</div>
|
|
<h2 className="text-lg font-semibold text-foreground">Keyboard Shortcuts</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 text-foreground-muted hover:text-foreground rounded-lg hover:bg-foreground/5 transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 max-h-[60vh] overflow-y-auto space-y-6">
|
|
{/* Navigation */}
|
|
{categories.navigation.length > 0 && (
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">Navigation</h3>
|
|
<div className="space-y-2">
|
|
{categories.navigation.map(shortcut => (
|
|
<ShortcutRow key={shortcut.key} shortcut={shortcut} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
{categories.actions.length > 0 && (
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">Actions</h3>
|
|
<div className="space-y-2">
|
|
{categories.actions.map(shortcut => (
|
|
<ShortcutRow key={shortcut.key} shortcut={shortcut} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Global */}
|
|
{categories.global.length > 0 && (
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">Global</h3>
|
|
<div className="space-y-2">
|
|
{categories.global.map(shortcut => (
|
|
<ShortcutRow key={shortcut.key} shortcut={shortcut} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-3 border-t border-border/50 bg-background-secondary/30">
|
|
<p className="text-xs text-foreground-subtle text-center">
|
|
Press <kbd className="px-1.5 py-0.5 bg-foreground/10 rounded text-foreground-muted">?</kbd> anytime to show this help
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ShortcutRow({ shortcut }: { shortcut: Shortcut }) {
|
|
return (
|
|
<div className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-foreground/5 transition-colors">
|
|
<div>
|
|
<p className="text-sm font-medium text-foreground">{shortcut.label}</p>
|
|
<p className="text-xs text-foreground-subtle">{shortcut.description}</p>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{shortcut.requiresModifier && (
|
|
<>
|
|
<kbd className="px-2 py-1 bg-foreground/10 rounded text-xs font-mono text-foreground-muted">⌘</kbd>
|
|
<span className="text-foreground-subtle">+</span>
|
|
</>
|
|
)}
|
|
<kbd className="px-2 py-1 bg-foreground/10 rounded text-xs font-mono text-foreground-muted uppercase">
|
|
{shortcut.key}
|
|
</kbd>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// USER BACKEND SHORTCUTS
|
|
// ============================================================================
|
|
|
|
export function useUserShortcuts() {
|
|
const router = useRouter()
|
|
const { registerShortcut, unregisterShortcut, setShowHelp } = useKeyboardShortcuts()
|
|
|
|
useEffect(() => {
|
|
const userShortcuts: Shortcut[] = [
|
|
// Navigation
|
|
{ key: 'g', label: 'Go to Dashboard', description: 'Navigate to dashboard', action: () => router.push('/dashboard'), category: 'navigation' },
|
|
{ key: 'w', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/watchlist'), category: 'navigation' },
|
|
{ key: 'p', label: 'Go to Portfolio', description: 'Navigate to portfolio', action: () => router.push('/portfolio'), category: 'navigation' },
|
|
{ key: 'a', label: 'Go to Auctions', description: 'Navigate to auctions', action: () => router.push('/auctions'), category: 'navigation' },
|
|
{ key: 'i', label: 'Go to Intelligence', description: 'Navigate to TLD intelligence', action: () => router.push('/intelligence'), category: 'navigation' },
|
|
{ key: 's', label: 'Go to Settings', description: 'Navigate to settings', action: () => router.push('/settings'), category: 'navigation' },
|
|
// Actions
|
|
{ key: 'n', label: 'Add Domain', description: 'Quick add a new domain', action: () => document.querySelector<HTMLInputElement>('input[placeholder*="domain"]')?.focus(), category: 'actions' },
|
|
{ key: 'k', label: 'Search', description: 'Focus search input', action: () => document.querySelector<HTMLInputElement>('input[type="text"]')?.focus(), category: 'actions', requiresModifier: true },
|
|
// Global
|
|
{ key: '?', label: 'Show Shortcuts', description: 'Display this help', action: () => setShowHelp(true), category: 'global' },
|
|
{ key: 'Escape', label: 'Close Modal', description: 'Close any open modal', action: () => {}, category: 'global' },
|
|
]
|
|
|
|
userShortcuts.forEach(registerShortcut)
|
|
|
|
return () => {
|
|
userShortcuts.forEach(s => unregisterShortcut(s.key))
|
|
}
|
|
}, [router, registerShortcut, unregisterShortcut, setShowHelp])
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADMIN SHORTCUTS
|
|
// ============================================================================
|
|
|
|
export function useAdminShortcuts() {
|
|
const router = useRouter()
|
|
const { registerShortcut, unregisterShortcut, setShowHelp } = useKeyboardShortcuts()
|
|
|
|
useEffect(() => {
|
|
const adminShortcuts: Shortcut[] = [
|
|
// Navigation
|
|
{ key: 'o', label: 'Overview', description: 'Go to admin overview', action: () => {}, category: 'navigation' },
|
|
{ key: 'u', label: 'Users', description: 'Go to users management', action: () => {}, category: 'navigation' },
|
|
{ key: 'b', label: 'Blog', description: 'Go to blog management', action: () => {}, category: 'navigation' },
|
|
{ key: 'y', label: 'System', description: 'Go to system status', action: () => {}, category: 'navigation' },
|
|
// Actions
|
|
{ key: 'r', label: 'Refresh Data', description: 'Refresh current data', action: () => window.location.reload(), category: 'actions' },
|
|
{ key: 'e', label: 'Export', description: 'Export current data', action: () => {}, category: 'actions' },
|
|
// Global
|
|
{ key: '?', label: 'Show Shortcuts', description: 'Display this help', action: () => setShowHelp(true), category: 'global' },
|
|
{ key: 'd', label: 'Back to Dashboard', description: 'Return to user dashboard', action: () => router.push('/dashboard'), category: 'global' },
|
|
]
|
|
|
|
adminShortcuts.forEach(registerShortcut)
|
|
|
|
return () => {
|
|
adminShortcuts.forEach(s => unregisterShortcut(s.key))
|
|
}
|
|
}, [router, registerShortcut, unregisterShortcut, setShowHelp])
|
|
}
|
|
|
|
// ============================================================================
|
|
// SHORTCUT HINT COMPONENT
|
|
// ============================================================================
|
|
|
|
export function ShortcutHint({ shortcut, className }: { shortcut: string; className?: string }) {
|
|
return (
|
|
<kbd className={clsx(
|
|
"hidden sm:inline-flex items-center justify-center",
|
|
"px-1.5 py-0.5 text-[10px] font-mono uppercase",
|
|
"bg-foreground/5 text-foreground-subtle border border-border/50 rounded",
|
|
className
|
|
)}>
|
|
{shortcut}
|
|
</kbd>
|
|
)
|
|
}
|
|
|