Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -163,6 +164,7 @@ const AnimatedRoutes = () => {

const App = () => {
return (
<KeyboardShortcutsProvider>
<BrowserRouter>
<Navbar />
<main className="app-bg min-h-screen pt-15 flex flex-col text-main transition-colors duration-300">
Expand Down Expand Up @@ -268,6 +270,7 @@ const App = () => {
<Footer />
<ScrollToTop />
</BrowserRouter>
</KeyboardShortcutsProvider>
);
};

Expand Down
47 changes: 47 additions & 0 deletions frontend/src/components/KeyboardShortcutsHelp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState, useEffect } from 'react';

Check failure on line 1 in frontend/src/components/KeyboardShortcutsHelp.jsx

View workflow job for this annotation

GitHub Actions / frontend

'useEffect' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 1 in frontend/src/components/KeyboardShortcutsHelp.jsx

View workflow job for this annotation

GitHub Actions / frontend

'useState' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Keyboard Shortcuts</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
&times;
</button>
</div>
<div className="space-y-3">
{shortcuts.map((shortcut, index) => (
<div key={index} className="flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-300">{shortcut.description}</span>
<div className="flex gap-1">
{shortcut.keys.map((key, keyIndex) => (
<kbd
key={keyIndex}
className="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm font-mono"
>
{key}
</kbd>
))}
</div>
</div>
))}
</div>
<div className="mt-4 text-sm text-gray-500">
Press <kbd className="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">?</kbd> to open this help
</div>
</div>
</div>
);
};

export default KeyboardShortcutsHelp;
66 changes: 66 additions & 0 deletions frontend/src/components/KeyboardShortcutsProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState, useEffect, createContext, useContext } from 'react';
import useKeyboardShortcuts from '../hooks/useKeyboardShortcuts';
import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';

const KeyboardShortcutsContext = createContext();

export const useKeyboardShortcutsContext = () => useContext(KeyboardShortcutsContext);

Check failure on line 7 in frontend/src/components/KeyboardShortcutsProvider.jsx

View workflow job for this annotation

GitHub Actions / frontend

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components

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 (
<KeyboardShortcutsContext.Provider value={{ showHelp, setShowHelp }}>
{children}
<KeyboardShortcutsHelp isOpen={showHelp} onClose={() => setShowHelp(false)} />
</KeyboardShortcutsContext.Provider>
);
};

export default KeyboardShortcutsProvider;
40 changes: 40 additions & 0 deletions frontend/src/hooks/useKeyboardShortcuts.js
Original file line number Diff line number Diff line change
@@ -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;
32 changes: 32 additions & 0 deletions src/components/TaskPriority.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

export type Priority = 'low' | 'medium' | 'high' | 'urgent';

interface TaskPriorityProps {
priority: Priority;
onChange?: (priority: Priority) => void;
disabled?: boolean;
}

const priorityColors: Record<Priority, string> = {
low: 'bg-gray-100 text-gray-700',
medium: 'bg-blue-100 text-blue-700',
high: 'bg-orange-100 text-orange-700',
urgent: 'bg-red-100 text-red-700',
};

export function TaskPriority({ priority, onChange, disabled }: TaskPriorityProps) {
return (
<select
value={priority}
onChange={(e) => onChange?.(e.target.value as Priority)}
disabled={disabled}
className={px-2 py-1 rounded-full text-xs font-medium }
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
);
}
Loading