From 0b43d130abeacc77a6844ac86410b87954c268bb Mon Sep 17 00:00:00 2001 From: zeroknowledge0x Date: Sun, 31 May 2026 00:06:32 +0000 Subject: [PATCH] feat: add keyboard shortcuts for rapid navigation (Fixes #640) - Add useKeyboardShortcuts hook with vim-style G+key navigation - Add ShortcutsHelp modal with categorized shortcut display - Support role-specific shortcuts (admin vs user) - Add Ctrl+K for search, Ctrl+/ for shortcuts help - Escape key closes modals - Visual keyboard key display with platform-aware formatting --- Frontend/src/App.jsx | 23 +- .../src/components/shared/ShortcutsHelp.jsx | 181 ++++++++++++++++ Frontend/src/hooks/useKeyboardShortcuts.js | 202 ++++++++++++++++++ 3 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 Frontend/src/components/shared/ShortcutsHelp.jsx create mode 100644 Frontend/src/hooks/useKeyboardShortcuts.js diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx index c0b21007..bec7c539 100644 --- a/Frontend/src/App.jsx +++ b/Frontend/src/App.jsx @@ -5,13 +5,15 @@ import { Navigate, useLocation } from "react-router-dom"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { AnimatePresence } from "framer-motion"; import { NotFound } from "./components/ui/not-found-2"; import useTicketStore from "./store/ticketStore"; import Toaster from "./components/shared/Toaster"; import BugReportWidget from "./components/shared/BugReportWidget"; import useRealtimeNotifications from "./hooks/useRealtimeNotifications"; +import useKeyboardShortcuts from "./hooks/useKeyboardShortcuts"; +import ShortcutsHelp from "./components/shared/ShortcutsHelp"; // Auth Components import Login from "./pages/Login"; @@ -151,10 +153,24 @@ function ScrollToTop() { function AppLayout() { const { user, profile } = useAuthStore(); + const [showShortcuts, setShowShortcuts] = useState(false); // Initialize Global Realtime Notifications Listener useRealtimeNotifications(); + // Initialize keyboard shortcuts + const { shortcuts } = useKeyboardShortcuts( + // Add role-specific shortcuts + profile?.role === 'admin' || profile?.role === 'super_admin' + ? { 'g,a': '/admin/dashboard', 'g,k': '/admin/tickets', 'g,u': '/admin/users', 'g,s': '/admin/settings' } + : profile?.role === 'master_admin' + ? { 'g,a': '/master-admin/dashboard', 'g,k': '/master-admin/admin-requests', 'g,u': '/master-admin/all-admins' } + : {}, + { + onShortcutsHelp: () => setShowShortcuts(true), + } + ); + useEffect(() => { if (!user) return; const handleFocus = () => { @@ -169,6 +185,11 @@ function AppLayout() { // but we still need to handle role-based navigation here return ( <> + setShowShortcuts(false)} + shortcuts={shortcuts} + /> } /> } /> diff --git a/Frontend/src/components/shared/ShortcutsHelp.jsx b/Frontend/src/components/shared/ShortcutsHelp.jsx new file mode 100644 index 00000000..31ea12b4 --- /dev/null +++ b/Frontend/src/components/shared/ShortcutsHelp.jsx @@ -0,0 +1,181 @@ +/** + * Shortcuts Help Modal + * Displays available keyboard shortcuts in a styled overlay. + */ + +import React, { useState, useEffect } from 'react'; +import { formatShortcut, getShortcutDescription } from '../hooks/useKeyboardShortcuts'; + +const ShortcutsHelp = ({ isOpen, onClose, shortcuts = {} }) => { + const [selectedCategory, setSelectedCategory] = useState('navigation'); + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'unset'; + } + return () => { + document.body.style.overflow = 'unset'; + }; + }, [isOpen]); + + useEffect(() => { + const handleEscape = (e) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + // Categorize shortcuts + const categories = { + navigation: { + title: 'Navigation', + icon: ( + + + + ), + shortcuts: ['g,d', 'g,t', 'g,n', 'g,p', 'g,h', 'g,a', 'g,k', 'g,u', 'g,s'], + }, + actions: { + title: 'Quick Actions', + icon: ( + + + + ), + shortcuts: ['ctrl+k', 'ctrl+/', 'escape'], + }, + }; + + // Filter shortcuts based on what's available + const getAvailableShortcuts = (categoryShortcuts) => { + return categoryShortcuts.filter(s => s in shortcuts); + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+
+
+ + + +
+
+

Keyboard Shortcuts

+

Navigate faster with keyboard shortcuts

+
+
+ +
+
+ + {/* Content */} +
+ {/* Category Tabs */} +
+ {Object.entries(categories).map(([key, category]) => { + const available = getAvailableShortcuts(category.shortcuts); + if (available.length === 0) return null; + + return ( + + ); + })} +
+ + {/* Shortcuts List */} +
+ {categories[selectedCategory]?.shortcuts.map(shortcut => { + if (!(shortcut in shortcuts)) return null; + + const description = getShortcutDescription(shortcut); + const formatted = formatShortcut(shortcut); + + return ( +
+ {description} +
+ {formatted.split('').map((char, index) => ( + + {char} + + ))} +
+
+ ); + })} +
+ + {/* Tips */} +
+

💡 Tips

+
    +
  • • Press G then wait for a second, then press the next key
  • +
  • • Shortcuts don't work when typing in input fields
  • +
  • • Press Esc to close any modal
  • +
+
+
+ + {/* Footer */} +
+
+

+ Press Ctrl + / to toggle this help +

+ +
+
+
+
+ ); +}; + +export default ShortcutsHelp; diff --git a/Frontend/src/hooks/useKeyboardShortcuts.js b/Frontend/src/hooks/useKeyboardShortcuts.js new file mode 100644 index 00000000..67c7c3ed --- /dev/null +++ b/Frontend/src/hooks/useKeyboardShortcuts.js @@ -0,0 +1,202 @@ +/** + * Keyboard Shortcuts Hook + * Provides global keyboard shortcuts for rapid navigation. + */ + +import { useEffect, useCallback, useRef } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; + +// Default shortcuts configuration +const DEFAULT_SHORTCUTS = { + // Navigation shortcuts (G + key) + 'g,d': '/dashboard', + 'g,t': '/my-tickets', + 'g,n': '/create-ticket', + 'g,p': '/profile', + 'g,h': '/help', + 'g,a': '/admin/dashboard', + 'g,k': '/admin/tickets', + 'g,u': '/admin/users', + 'g,s': '/admin/settings', + + // Quick actions + 'ctrl+k': 'search', + 'ctrl+/': 'shortcuts-help', + 'escape': 'close-modal', +}; + +/** + * Hook to register keyboard shortcuts + * @param {Object} customShortcuts - Additional shortcuts to merge with defaults + * @param {Object} options - Configuration options + * @param {boolean} options.enabled - Whether shortcuts are enabled (default: true) + * @param {Function} options.onSearch - Callback for search shortcut + * @param {Function} options.onShortcutsHelp - Callback for shortcuts help + */ +export const useKeyboardShortcuts = (customShortcuts = {}, options = {}) => { + const { + enabled = true, + onSearch = null, + onShortcutsHelp = null, + } = options; + + const navigate = useNavigate(); + const location = useLocation(); + const pendingKeyRef = useRef(null); + const timeoutRef = useRef(null); + + // Merge default and custom shortcuts + const shortcuts = { ...DEFAULT_SHORTCUTS, ...customShortcuts }; + + const handleKeyDown = useCallback((event) => { + if (!enabled) return; + + // Don't trigger shortcuts when typing in inputs + const target = event.target; + if ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.tagName === 'SELECT' || + target.isContentEditable + ) { + return; + } + + const key = event.key.toLowerCase(); + const ctrl = event.ctrlKey || event.metaKey; + const shift = event.shiftKey; + const alt = event.altKey; + + // Handle Escape key + if (key === 'escape') { + const action = shortcuts['escape']; + if (action === 'close-modal') { + // Close any open modals + document.querySelectorAll('[data-modal]').forEach(modal => { + modal.click(); + }); + } + pendingKeyRef.current = null; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } + + // Handle Ctrl+K (search) + if (ctrl && key === 'k') { + event.preventDefault(); + if (onSearch) { + onSearch(); + } + return; + } + + // Handle Ctrl+/ (shortcuts help) + if (ctrl && key === '/') { + event.preventDefault(); + if (onShortcutsHelp) { + onShortcutsHelp(); + } + return; + } + + // Handle G + key combinations (vim-style navigation) + if (pendingKeyRef.current === 'g') { + const combo = `g,${key}`; + const target = shortcuts[combo]; + + if (target) { + event.preventDefault(); + navigate(target); + } + + pendingKeyRef.current = null; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } + + // Set pending key for G combinations + if (key === 'g' && !ctrl && !shift && !alt) { + pendingKeyRef.current = 'g'; + timeoutRef.current = setTimeout(() => { + pendingKeyRef.current = null; + }, 1000); // 1 second timeout for key sequence + return; + } + }, [enabled, shortcuts, navigate, location, onSearch, onShortcutsHelp]); + + useEffect(() => { + if (!enabled) return; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [enabled, handleKeyDown]); + + return { + shortcuts, + pendingKey: pendingKeyRef.current, + }; +}; + +/** + * Get shortcut display string + * @param {string} shortcut - Shortcut key combination + * @returns {string} - Formatted string for display + */ +export const formatShortcut = (shortcut) => { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + + return shortcut + .split('+') + .map(key => { + switch (key.toLowerCase()) { + case 'ctrl': + return isMac ? '⌘' : 'Ctrl'; + case 'shift': + return isMac ? '⇧' : 'Shift'; + case 'alt': + return isMac ? '⌥' : 'Alt'; + case 'g': + return 'G'; + default: + return key.toUpperCase(); + } + }) + .join(isMac ? '' : '+'); +}; + +/** + * Get shortcut description + * @param {string} shortcut - Shortcut key combination + * @returns {string} - Human-readable description + */ +export const getShortcutDescription = (shortcut) => { + const descriptions = { + 'g,d': 'Go to Dashboard', + 'g,t': 'Go to My Tickets', + 'g,n': 'Create New Ticket', + 'g,p': 'Go to Profile', + 'g,h': 'Go to Help', + 'g,a': 'Go to Admin Dashboard', + 'g,k': 'Go to Admin Tickets', + 'g,u': 'Go to Admin Users', + 'g,s': 'Go to Admin Settings', + 'ctrl+k': 'Open Search', + 'ctrl+/': 'Show Shortcuts', + 'escape': 'Close Modal', + }; + + return descriptions[shortcut] || shortcut; +}; + +export default useKeyboardShortcuts;