pounce/frontend/src/hooks/useKeyboardShortcuts.tsx
yves.gugger a4df5a8487 feat: Complete redesign of user and admin backend with consistent styling
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
2025-12-10 10:23:40 +01:00

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>
)
}