From cda9d90bf34f18ca431a7824154c973b7e1f02b1 Mon Sep 17 00:00:00 2001 From: Mohye24k Date: Mon, 13 Apr 2026 22:36:29 +0200 Subject: [PATCH] feat: add keyboard shortcuts hook with navigation and search bindings Add useKeyboardShortcuts React hook registering Ctrl+E (expenses), Ctrl+B (bills), Ctrl+D (dashboard), Ctrl+K (search), and Escape (close). Includes ShortcutConfig type definition. Fixes #106 --- app/src/api/shortcuts.ts | 14 ++++++++++++ app/src/hooks/useKeyboardShortcuts.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 app/src/api/shortcuts.ts create mode 100644 app/src/hooks/useKeyboardShortcuts.ts diff --git a/app/src/api/shortcuts.ts b/app/src/api/shortcuts.ts new file mode 100644 index 000000000..8f1b3fb0d --- /dev/null +++ b/app/src/api/shortcuts.ts @@ -0,0 +1,14 @@ +export type ShortcutConfig = { + key: string; + ctrlKey: boolean; + description: string; + action: string; +}; + +export const DEFAULT_SHORTCUTS: ShortcutConfig[] = [ + { key: 'e', ctrlKey: true, description: 'Go to Expenses', action: 'navigate:/expenses' }, + { key: 'b', ctrlKey: true, description: 'Go to Bills', action: 'navigate:/bills' }, + { key: 'd', ctrlKey: true, description: 'Go to Dashboard', action: 'navigate:/dashboard' }, + { key: 'k', ctrlKey: true, description: 'Open Search', action: 'open:search' }, + { key: 'Escape', ctrlKey: false, description: 'Close current dialog', action: 'close:modal' }, +]; diff --git a/app/src/hooks/useKeyboardShortcuts.ts b/app/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 000000000..d68b0fb87 --- /dev/null +++ b/app/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,33 @@ +import { useEffect, useCallback } from 'react'; +import { ShortcutConfig, DEFAULT_SHORTCUTS } from '../api/shortcuts'; + +export type ShortcutHandler = (action: string) => void; + +export function useKeyboardShortcuts(onAction: ShortcutHandler) { + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + for (const shortcut of DEFAULT_SHORTCUTS) { + const ctrlMatch = shortcut.ctrlKey + ? event.ctrlKey || event.metaKey + : true; + const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase(); + + if (ctrlMatch && keyMatch && (shortcut.ctrlKey === (event.ctrlKey || event.metaKey))) { + event.preventDefault(); + onAction(shortcut.action); + return; + } + } + }, + [onAction], + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + + return DEFAULT_SHORTCUTS; +}