diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index def42c19..9914ef12 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -2,6 +2,7 @@ import React from "react";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
import { AnimatePresence } from "framer-motion";
import Navbar from "./components/Navbar.jsx";
+import KeyboardShortcutsProvider from "./components/KeyboardShortcutsProvider.jsx";
import LandingPage from "./pages/LandingPage";
import Login from "./pages/Login.jsx";
import Signup from "./pages/Signup.jsx";
@@ -170,6 +171,7 @@ const AnimatedRoutes = () => {
const App = () => {
return (
+
@@ -281,6 +283,7 @@ const App = () => {
+
);
};
diff --git a/frontend/src/components/KeyboardShortcutsHelp.jsx b/frontend/src/components/KeyboardShortcutsHelp.jsx
new file mode 100644
index 00000000..cf27a5c4
--- /dev/null
+++ b/frontend/src/components/KeyboardShortcutsHelp.jsx
@@ -0,0 +1,45 @@
+const shortcuts = [
+ { keys: ['Ctrl', 'N'], description: 'New task' },
+ { keys: ['Ctrl', 'K'], description: 'Toggle search' },
+ { keys: ['Ctrl', 'D'], description: 'Toggle dark mode' },
+ { keys: ['Esc'], description: 'Close modal' },
+];
+
+const KeyboardShortcutsHelp = ({ isOpen, onClose }) => {
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
Keyboard Shortcuts
+
+
+
+ {shortcuts.map((shortcut, index) => (
+
+
{shortcut.description}
+
+ {shortcut.keys.map((key, keyIndex) => (
+
+ {key}
+
+ ))}
+
+
+ ))}
+
+
+ Press ? to open this help
+
+
+
+ );
+};
+
+export default KeyboardShortcutsHelp;
diff --git a/frontend/src/components/KeyboardShortcutsProvider.jsx b/frontend/src/components/KeyboardShortcutsProvider.jsx
new file mode 100644
index 00000000..5300bfdb
--- /dev/null
+++ b/frontend/src/components/KeyboardShortcutsProvider.jsx
@@ -0,0 +1,64 @@
+import { useState, useEffect, createContext } from 'react';
+import useKeyboardShortcuts from '../hooks/useKeyboardShortcuts';
+import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';
+
+const KeyboardShortcutsContext = createContext();
+
+const KeyboardShortcutsProvider = ({ children }) => {
+ const [showHelp, setShowHelp] = useState(false);
+
+ const handleNewTask = () => {
+ // Navigate to tasks page or open new task modal
+ window.dispatchEvent(new CustomEvent('keyboard-shortcut', { detail: 'new-task' }));
+ };
+
+ const handleToggleSearch = () => {
+ window.dispatchEvent(new CustomEvent('keyboard-shortcut', { detail: 'toggle-search' }));
+ };
+
+ const handleToggleDarkMode = () => {
+ const html = document.documentElement;
+ const isDark = html.classList.contains('dark');
+ if (isDark) {
+ html.classList.remove('dark');
+ localStorage.setItem('theme', 'light');
+ } else {
+ html.classList.add('dark');
+ localStorage.setItem('theme', 'dark');
+ }
+ };
+
+ const handleCloseModal = () => {
+ window.dispatchEvent(new CustomEvent('keyboard-shortcut', { detail: 'close-modal' }));
+ };
+
+ useKeyboardShortcuts({
+ onNewTask: handleNewTask,
+ onToggleSearch: handleToggleSearch,
+ onToggleDarkMode: handleToggleDarkMode,
+ onCloseModal: handleCloseModal,
+ });
+
+ // Handle '?' key for help
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
+ return;
+ }
+ if (e.key === '?') {
+ setShowHelp(prev => !prev);
+ }
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
+
+ return (
+
+ {children}
+ setShowHelp(false)} />
+
+ );
+};
+
+export default KeyboardShortcutsProvider;
diff --git a/frontend/src/hooks/useKeyboardShortcuts.js b/frontend/src/hooks/useKeyboardShortcuts.js
new file mode 100644
index 00000000..ebbad8d0
--- /dev/null
+++ b/frontend/src/hooks/useKeyboardShortcuts.js
@@ -0,0 +1,40 @@
+import { useEffect, useCallback } from 'react';
+
+const useKeyboardShortcuts = ({ onNewTask, onToggleSearch, onToggleDarkMode, onCloseModal }) => {
+ const handleKeyDown = useCallback((e) => {
+ // Ignore if user is typing in an input
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
+ return;
+ }
+
+ // Ctrl/Cmd + N: New task
+ if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
+ e.preventDefault();
+ onNewTask?.();
+ }
+
+ // Ctrl/Cmd + K: Toggle search
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ onToggleSearch?.();
+ }
+
+ // Ctrl/Cmd + D: Toggle dark mode
+ if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
+ e.preventDefault();
+ onToggleDarkMode?.();
+ }
+
+ // Escape: Close modal
+ if (e.key === 'Escape') {
+ onCloseModal?.();
+ }
+ }, [onNewTask, onToggleSearch, onToggleDarkMode, onCloseModal]);
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [handleKeyDown]);
+};
+
+export default useKeyboardShortcuts;