feat: integrate global keyboard shortcuts across multiple components
- Added `KeyboardShortcutsProvider` to `RootLayout` for centralized keyboard shortcut management. - Implemented `useGlobalKeyboardShortcuts` in `DailyPageClient`, `KanbanPageClient`, and `HomePageClient` to enhance navigation and task management with keyboard shortcuts. - Updated `KeyboardShortcuts` component to render a modal for displaying available shortcuts, improving user accessibility. - Enhanced `Header` component with buttons to open the keyboard shortcuts modal, streamlining user interaction.
This commit is contained in:
@@ -14,6 +14,7 @@ import { PendingTasksSection } from '@/components/daily/PendingTasksSection';
|
|||||||
import { dailyClient } from '@/clients/daily-client';
|
import { dailyClient } from '@/clients/daily-client';
|
||||||
import { Header } from '@/components/ui/Header';
|
import { Header } from '@/components/ui/Header';
|
||||||
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
||||||
|
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
|
||||||
|
|
||||||
interface DailyPageClientProps {
|
interface DailyPageClientProps {
|
||||||
initialDailyView?: DailyView;
|
initialDailyView?: DailyView;
|
||||||
@@ -85,6 +86,13 @@ export function DailyPageClient({
|
|||||||
await refreshDailyDates();
|
await refreshDailyDates();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Raccourcis clavier globaux pour la page Daily
|
||||||
|
useGlobalKeyboardShortcuts({
|
||||||
|
onNavigatePrevious: goToPreviousDay,
|
||||||
|
onNavigateNext: goToNextDay,
|
||||||
|
onGoToToday: goToToday
|
||||||
|
});
|
||||||
|
|
||||||
const handleToggleCheckbox = async (checkboxId: string) => {
|
const handleToggleCheckbox = async (checkboxId: string) => {
|
||||||
await toggleCheckbox(checkboxId);
|
await toggleCheckbox(checkboxId);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
|
|||||||
import { MobileControls } from '@/components/kanban/MobileControls';
|
import { MobileControls } from '@/components/kanban/MobileControls';
|
||||||
import { DesktopControls } from '@/components/kanban/DesktopControls';
|
import { DesktopControls } from '@/components/kanban/DesktopControls';
|
||||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||||
|
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
|
||||||
|
|
||||||
interface KanbanPageClientProps {
|
interface KanbanPageClientProps {
|
||||||
initialTasks: Task[];
|
initialTasks: Task[];
|
||||||
@@ -55,6 +56,19 @@ function KanbanPageContent() {
|
|||||||
setIsCreateModalOpen(false);
|
setIsCreateModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Raccourcis clavier globaux pour la page Kanban
|
||||||
|
useGlobalKeyboardShortcuts({
|
||||||
|
onCreateTask: () => setIsCreateModalOpen(true),
|
||||||
|
onToggleFilters: handleToggleFilters,
|
||||||
|
onToggleCompactView: handleToggleCompactView,
|
||||||
|
onToggleSwimlanes: handleToggleSwimlanes,
|
||||||
|
onOpenSearch: () => {
|
||||||
|
// Focus sur le champ de recherche dans les contrôles desktop
|
||||||
|
const searchInput = document.querySelector('input[placeholder*="Rechercher"]') as HTMLInputElement;
|
||||||
|
searchInput?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--background)]">
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
<Header
|
<Header
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "./globals.css";
|
|||||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||||
import { JiraConfigProvider } from "@/contexts/JiraConfigContext";
|
import { JiraConfigProvider } from "@/contexts/JiraConfigContext";
|
||||||
import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
|
import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
|
||||||
|
import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
|
||||||
import { userPreferencesService } from "@/services/core/user-preferences";
|
import { userPreferencesService } from "@/services/core/user-preferences";
|
||||||
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
||||||
|
|
||||||
@@ -39,12 +40,14 @@ export default async function RootLayout({
|
|||||||
initialTheme={initialPreferences.viewPreferences.theme}
|
initialTheme={initialPreferences.viewPreferences.theme}
|
||||||
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
||||||
>
|
>
|
||||||
|
<KeyboardShortcutsProvider>
|
||||||
<KeyboardShortcuts />
|
<KeyboardShortcuts />
|
||||||
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
||||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||||
{children}
|
{children}
|
||||||
</UserPreferencesProvider>
|
</UserPreferencesProvider>
|
||||||
</JiraConfigProvider>
|
</JiraConfigProvider>
|
||||||
|
</KeyboardShortcutsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { RecentTasks } from '@/components/dashboard/RecentTasks';
|
|||||||
import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics';
|
import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics';
|
||||||
import { ProductivityMetrics } from '@/services/analytics/analytics';
|
import { ProductivityMetrics } from '@/services/analytics/analytics';
|
||||||
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
||||||
|
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
|
||||||
|
|
||||||
interface HomePageClientProps {
|
interface HomePageClientProps {
|
||||||
initialTasks: Task[];
|
initialTasks: Task[];
|
||||||
@@ -31,6 +32,20 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: {
|
|||||||
await createTask(data);
|
await createTask(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Raccourcis clavier globaux pour la page Dashboard
|
||||||
|
useGlobalKeyboardShortcuts({
|
||||||
|
onOpenSearch: () => {
|
||||||
|
// Focus sur le champ de recherche s'il existe, sinon naviguer vers Kanban
|
||||||
|
const searchInput = document.querySelector('input[placeholder*="Rechercher"]') as HTMLInputElement;
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.focus();
|
||||||
|
} else {
|
||||||
|
// Naviguer vers Kanban où il y a une recherche
|
||||||
|
window.location.href = '/kanban';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--background)]">
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
<Header
|
<Header
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||||
|
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
||||||
|
import { KeyboardShortcutsModal } from '@/components/ui/KeyboardShortcutsModal';
|
||||||
|
|
||||||
export function KeyboardShortcuts() {
|
export function KeyboardShortcuts() {
|
||||||
useKeyboardShortcuts();
|
useKeyboardShortcuts();
|
||||||
return null; // Ce composant ne rend rien, il gère juste les raccourcis
|
const { isOpen, shortcuts, closeModal } = useKeyboardShortcutsModal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardShortcutsModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeModal}
|
||||||
|
shortcuts={shortcuts}
|
||||||
|
title="Raccourcis clavier"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Link from 'next/link';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Theme } from '@/lib/theme-config';
|
import { Theme } from '@/lib/theme-config';
|
||||||
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
||||||
|
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -20,6 +21,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
|
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
|
||||||
|
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
|
||||||
|
|
||||||
// Liste des thèmes disponibles avec leurs labels et icônes
|
// Liste des thèmes disponibles avec leurs labels et icônes
|
||||||
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
|
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
|
||||||
@@ -97,6 +99,15 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
|
|
||||||
{/* Controls mobile/tablette */}
|
{/* Controls mobile/tablette */}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{/* Keyboard Shortcuts */}
|
||||||
|
<button
|
||||||
|
onClick={openShortcutsModal}
|
||||||
|
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
|
||||||
|
title="Raccourcis clavier (Cmd+?)"
|
||||||
|
>
|
||||||
|
⌨️
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Theme Dropdown */}
|
{/* Theme Dropdown */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
@@ -197,6 +208,15 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Keyboard Shortcuts desktop */}
|
||||||
|
<button
|
||||||
|
onClick={openShortcutsModal}
|
||||||
|
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
|
||||||
|
title="Raccourcis clavier (Cmd+?)"
|
||||||
|
>
|
||||||
|
⌨️
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Theme Dropdown desktop */}
|
{/* Theme Dropdown desktop */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
|
|||||||
96
src/components/ui/KeyboardShortcutsModal.tsx
Normal file
96
src/components/ui/KeyboardShortcutsModal.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface KeyboardShortcut {
|
||||||
|
keys: string[];
|
||||||
|
description: string;
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyboardShortcutsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
shortcuts: KeyboardShortcut[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeyBadge({ keyText }: { keyText: string }) {
|
||||||
|
return (
|
||||||
|
<kbd className="inline-flex items-center px-2 py-1 text-xs font-mono font-medium text-[var(--foreground)] bg-[var(--card)] border border-[var(--border)] rounded-md shadow-sm">
|
||||||
|
{keyText}
|
||||||
|
</kbd>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShortcutRow({ shortcut }: { shortcut: KeyboardShortcut }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-[var(--card-hover)] transition-colors">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{shortcut.keys.map((key, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-1">
|
||||||
|
<KeyBadge keyText={key} />
|
||||||
|
{index < shortcut.keys.length - 1 && (
|
||||||
|
<span className="text-[var(--muted-foreground)] text-xs">+</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-[var(--foreground)] font-mono">
|
||||||
|
{shortcut.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyboardShortcutsModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
shortcuts,
|
||||||
|
title = "Raccourcis clavier"
|
||||||
|
}: KeyboardShortcutsModalProps) {
|
||||||
|
// Grouper les raccourcis par catégorie
|
||||||
|
const groupedShortcuts = shortcuts.reduce((acc, shortcut) => {
|
||||||
|
const category = shortcut.category || 'Général';
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = [];
|
||||||
|
}
|
||||||
|
acc[category].push(shortcut);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, KeyboardShortcut[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} title={title} size="lg">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
|
||||||
|
<div key={category}>
|
||||||
|
<h3 className="text-sm font-mono font-semibold text-[var(--primary)] uppercase tracking-wider mb-3">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{categoryShortcuts.map((shortcut, index) => (
|
||||||
|
<ShortcutRow key={index} shortcut={shortcut} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{shortcuts.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-[var(--muted-foreground)] font-mono">
|
||||||
|
Aucun raccourci disponible sur cette page
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer avec info */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-[var(--border)]/50">
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] font-mono text-center">
|
||||||
|
Appuyez sur <KeyBadge keyText="Esc" /> pour fermer cette fenêtre
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
210
src/contexts/KeyboardShortcutsContext.tsx
Normal file
210
src/contexts/KeyboardShortcutsContext.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
export interface KeyboardShortcut {
|
||||||
|
keys: string[];
|
||||||
|
description: string;
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageShortcuts {
|
||||||
|
[pagePath: string]: KeyboardShortcut[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration des raccourcis par page
|
||||||
|
const PAGE_SHORTCUTS: PageShortcuts = {
|
||||||
|
// Raccourcis globaux (toutes les pages)
|
||||||
|
'*': [
|
||||||
|
{
|
||||||
|
keys: ['Cmd', '?'],
|
||||||
|
description: 'Afficher les raccourcis clavier',
|
||||||
|
category: 'Navigation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'D'],
|
||||||
|
description: 'Basculer le thème',
|
||||||
|
category: 'Apparence'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'T'],
|
||||||
|
description: 'Changer de thème sombre',
|
||||||
|
category: 'Apparence'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Esc'],
|
||||||
|
description: 'Fermer les modales/annuler',
|
||||||
|
category: 'Navigation'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
'/': [
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'K'],
|
||||||
|
description: 'Vers Kanban',
|
||||||
|
category: 'Navigation'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Kanban
|
||||||
|
'/kanban': [
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'N'],
|
||||||
|
description: 'Créer une nouvelle tâche',
|
||||||
|
category: 'Actions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Space'],
|
||||||
|
description: 'Basculer la vue compacte',
|
||||||
|
category: 'Vue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'F'],
|
||||||
|
description: 'Ouvrir les filtres',
|
||||||
|
category: 'Filtres'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'S'],
|
||||||
|
description: 'Basculer les swimlanes',
|
||||||
|
category: 'Vue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Tab'],
|
||||||
|
description: 'Navigation entre colonnes',
|
||||||
|
category: 'Navigation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Enter'],
|
||||||
|
description: 'Valider une tâche',
|
||||||
|
category: 'Actions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Esc'],
|
||||||
|
description: 'Annuler la création de tâche',
|
||||||
|
category: 'Actions'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Daily
|
||||||
|
'/daily': [
|
||||||
|
{
|
||||||
|
keys: ['←', '→'],
|
||||||
|
description: 'Navigation jour précédent/suivant',
|
||||||
|
category: 'Navigation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'T'],
|
||||||
|
description: 'Aller à aujourd\'hui',
|
||||||
|
category: 'Navigation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Enter'],
|
||||||
|
description: 'Valider un élément',
|
||||||
|
category: 'Actions'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Manager
|
||||||
|
'/weekly-manager': [
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'N'],
|
||||||
|
description: 'Créer un objectif',
|
||||||
|
category: 'Actions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['←', '→'],
|
||||||
|
description: 'Navigation semaine précédente/suivante',
|
||||||
|
category: 'Navigation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ['Cmd', 'Ctrl', 'T'],
|
||||||
|
description: 'Aller à cette semaine',
|
||||||
|
category: 'Navigation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
interface KeyboardShortcutsContextType {
|
||||||
|
isOpen: boolean;
|
||||||
|
shortcuts: KeyboardShortcut[];
|
||||||
|
openModal: () => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
toggleModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyboardShortcutsContext = createContext<KeyboardShortcutsContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface KeyboardShortcutsProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyboardShortcutsProvider({ children }: KeyboardShortcutsProviderProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Obtenir les raccourcis pour la page actuelle
|
||||||
|
const getCurrentPageShortcuts = (): KeyboardShortcut[] => {
|
||||||
|
const shortcuts: KeyboardShortcut[] = [];
|
||||||
|
|
||||||
|
// Ajouter les raccourcis globaux
|
||||||
|
if (PAGE_SHORTCUTS['*']) {
|
||||||
|
shortcuts.push(...PAGE_SHORTCUTS['*']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les raccourcis spécifiques à la page
|
||||||
|
const pageShortcuts = PAGE_SHORTCUTS[pathname];
|
||||||
|
if (pageShortcuts) {
|
||||||
|
shortcuts.push(...pageShortcuts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortcuts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shortcuts = getCurrentPageShortcuts();
|
||||||
|
|
||||||
|
// Gérer le raccourci Cmd+? pour ouvrir la popup
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Cmd+? ou Ctrl+? pour ouvrir les raccourcis
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === '?') {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esc pour fermer
|
||||||
|
if (event.key === 'Escape' && isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const contextValue: KeyboardShortcutsContextType = {
|
||||||
|
isOpen,
|
||||||
|
shortcuts,
|
||||||
|
openModal: () => setIsOpen(true),
|
||||||
|
closeModal: () => setIsOpen(false),
|
||||||
|
toggleModal: () => setIsOpen(prev => !prev)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardShortcutsContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</KeyboardShortcutsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeyboardShortcutsModal() {
|
||||||
|
const context = useContext(KeyboardShortcutsContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useKeyboardShortcutsModal must be used within a KeyboardShortcutsProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
102
src/hooks/useGlobalKeyboardShortcuts.ts
Normal file
102
src/hooks/useGlobalKeyboardShortcuts.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
interface KeyboardShortcutsActions {
|
||||||
|
onCreateTask?: () => void;
|
||||||
|
onToggleFilters?: () => void;
|
||||||
|
onToggleCompactView?: () => void;
|
||||||
|
onToggleSwimlanes?: () => void;
|
||||||
|
onNavigatePrevious?: () => void;
|
||||||
|
onNavigateNext?: () => void;
|
||||||
|
onGoToToday?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
onExport?: () => void;
|
||||||
|
onShowAnalytics?: () => void;
|
||||||
|
onSave?: () => void;
|
||||||
|
onBackup?: () => void;
|
||||||
|
onOpenSearch?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGlobalKeyboardShortcuts(actions: KeyboardShortcutsActions = {}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Éviter les raccourcis si on est dans un input/textarea
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true') {
|
||||||
|
// Permettre seulement certains raccourcis dans les inputs
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
// Esc pour fermer les modales même dans les inputs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ignorer les autres raccourcis dans les inputs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd+K - Ouvrir la recherche rapide
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onOpenSearch?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd+N - Créer une nouvelle tâche/élément
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'n') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onCreateTask?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd+F - Ouvrir les filtres (Kanban)
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'f') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onToggleFilters?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd+S - Basculer les swimlanes (Kanban)
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onToggleSwimlanes?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space - Basculer la vue compacte (Kanban)
|
||||||
|
if (event.key === ' ' && pathname === '/kanban') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onToggleCompactView?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd+T - Aller à aujourd'hui/cette semaine
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 't') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onGoToToday?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flèches gauche/droite - Navigation
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onNavigatePrevious?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowRight') {
|
||||||
|
event.preventDefault();
|
||||||
|
actions.onNavigateNext?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [pathname, actions]);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ export function useKeyboardShortcuts() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
// Cmd + T pour basculer entre light et le thème dark préféré
|
// Cmd + Shift + D pour basculer entre light et le thème dark préféré
|
||||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'D') {
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'D') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
toggleTheme();
|
toggleTheme();
|
||||||
|
|||||||
Reference in New Issue
Block a user