diff --git a/src/components/KeyboardShortcutsHelp.tsx b/src/components/KeyboardShortcutsHelp.tsx new file mode 100644 index 0000000..deb161a --- /dev/null +++ b/src/components/KeyboardShortcutsHelp.tsx @@ -0,0 +1,82 @@ +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Keyboard } from "lucide-react"; +import { type KeyboardShortcut, formatShortcut } from "@/hooks/useKeyboardShortcuts"; + +interface KeyboardShortcutsHelpProps { + open: boolean; + onOpenChange: (open: boolean) => void; + shortcuts: KeyboardShortcut[]; +} + +export function KeyboardShortcutsHelp({ open, onOpenChange, shortcuts }: KeyboardShortcutsHelpProps) { + // Group shortcuts by category + const groupedShortcuts = shortcuts.reduce((acc, shortcut) => { + // Determine category based on description + let category = 'Général'; + + if (shortcut.description.toLowerCase().includes('note')) { + category = 'Notes'; + } else if (shortcut.description.toLowerCase().includes('recherch')) { + category = 'Recherche'; + } else if (shortcut.description.toLowerCase().includes('todo') || shortcut.description.toLowerCase().includes('tâche')) { + category = 'Tâches'; + } else if (shortcut.description.toLowerCase().includes('navigation')) { + category = 'Navigation'; + } else if (shortcut.description.toLowerCase().includes('édition') || shortcut.description.toLowerCase().includes('gras') || shortcut.description.toLowerCase().includes('italique')) { + category = 'Édition'; + } + + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(shortcut); + return acc; + }, {} as Record); + + return ( + + + + + + Raccourcis clavier + + + Utilisez ces raccourcis pour naviguer plus rapidement + + + + +
+ {Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => ( +
+

+ {category} +

+
+ {categoryShortcuts.map((shortcut, index) => ( +
+ {shortcut.description} + + {formatShortcut(shortcut)} + +
+ ))} +
+
+ ))} +
+
+ +
+ Appuyez sur ? pour afficher cette aide +
+
+
+ ); +} diff --git a/src/components/StatsDashboard.tsx b/src/components/StatsDashboard.tsx new file mode 100644 index 0000000..681c412 --- /dev/null +++ b/src/components/StatsDashboard.tsx @@ -0,0 +1,204 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { + FileText, + CheckSquare, + Tag as TagIcon, + TrendingUp, + Calendar as CalendarIcon, + Clock, + Target, + Zap +} from "lucide-react"; +import { Note } from "@/services/NotesService"; +import { Todo } from "@/services/TodosService"; + +interface StatsDashboardProps { + notes: Note[]; + todos: Todo[]; +} + +export function StatsDashboard({ notes, todos }: StatsDashboardProps) { + // Calculate statistics + const totalNotes = notes.length; + const archivedNotes = notes.filter(n => n.archived).length; + const activeNotes = totalNotes - archivedNotes; + const priorityNotes = notes.filter(n => n.priority && !n.archived).length; + + const totalTodos = todos.length; + const completedTodos = todos.filter(t => t.completed).length; + const activeTodos = totalTodos - completedTodos; + const completionRate = totalTodos > 0 ? (completedTodos / totalTodos) * 100 : 0; + + // Notes with specific content + const notesWithTodos = notes.filter(n => n.todos && n.todos.length > 0 && !n.archived).length; + const notesWithImages = notes.filter(n => n.images && n.images.length > 0 && !n.archived).length; + const notesWithFiles = notes.filter(n => n.files && n.files.length > 0 && !n.archived).length; + + // Get all unique tags + const allTags = new Set(); + notes.forEach(note => { + note.tags?.forEach(tag => allTags.add(tag.name)); + }); + const totalTags = allTags.size; + + // Recent activity (notes updated in last 7 days) + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + const recentlyUpdated = notes.filter(n => + n.updated_at && new Date(n.updated_at) > sevenDaysAgo + ).length; + + // Calculate productivity score (0-100) + const productivityScore = Math.round( + (completionRate * 0.4) + + ((activeNotes > 0 ? Math.min(priorityNotes / activeNotes, 1) : 0) * 20) + + ((recentlyUpdated / Math.max(activeNotes, 1)) * 40) + ); + + return ( +
+ {/* Productivity Score */} + + + + + Score de Productivité + + + +
+
+ {productivityScore} + = 70 ? "default" : productivityScore >= 40 ? "secondary" : "outline"}> + {productivityScore >= 70 ? "Excellent" : productivityScore >= 40 ? "Bien" : "À améliorer"} + +
+ +

+ Basé sur le taux de complétion, notes prioritaires et activité récente +

+
+
+
+ + {/* Notes Statistics */} +
+ + + + + Notes + + + +
+
+ Total + {totalNotes} +
+
+ Actives + {activeNotes} +
+
+ Archivées + {archivedNotes} +
+
+ Prioritaires + {priorityNotes} +
+
+
+
+ + + + + + Tâches + + + +
+
+ Total + {totalTodos} +
+
+ Complétées + {completedTodos} +
+
+ En cours + {activeTodos} +
+
+ Taux + {completionRate.toFixed(0)}% +
+
+
+
+
+ + {/* Content Statistics */} + + + + + Contenu + + + +
+
+
+ Avec tâches + {notesWithTodos} +
+
+ Avec images + {notesWithImages} +
+
+
+
+ Avec fichiers + {notesWithFiles} +
+
+ Tags uniques + {totalTags} +
+
+
+
+
+ + {/* Recent Activity */} + + + + + Activité Récente + + + +
+
+ + Derniers 7 jours +
+
+ {recentlyUpdated} + notes modifiées +
+
+
+
+
+ ); +} diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..89032a4 --- /dev/null +++ b/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,87 @@ +import { useEffect } from 'react'; + +export interface KeyboardShortcut { + key: string; + ctrl?: boolean; + alt?: boolean; + shift?: boolean; + meta?: boolean; + description: string; + handler: () => void; + preventDefault?: boolean; +} + +/** + * Custom hook for managing keyboard shortcuts + */ +export function useKeyboardShortcuts( + shortcuts: KeyboardShortcut[], + enabled = true +) { + useEffect(() => { + if (!enabled) return; + + const handleKeyDown = (event: KeyboardEvent) => { + for (const shortcut of shortcuts) { + const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase(); + const ctrlMatch = shortcut.ctrl ? (event.ctrlKey || event.metaKey) : !event.ctrlKey && !event.metaKey; + const altMatch = shortcut.alt ? event.altKey : !event.altKey; + const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey; + const metaMatch = shortcut.meta ? event.metaKey : !event.metaKey; + + if (keyMatch && ctrlMatch && altMatch && shiftMatch && metaMatch) { + if (shortcut.preventDefault !== false) { + event.preventDefault(); + } + shortcut.handler(); + break; + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [shortcuts, enabled]); +} + +/** + * Format keyboard shortcut for display + */ +export function formatShortcut(shortcut: KeyboardShortcut): string { + const parts: string[] = []; + + if (shortcut.ctrl || shortcut.meta) { + parts.push(navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'); + } + if (shortcut.alt) { + parts.push(navigator.platform.includes('Mac') ? '⌥' : 'Alt'); + } + if (shortcut.shift) { + parts.push(navigator.platform.includes('Mac') ? '⇧' : 'Shift'); + } + + parts.push(shortcut.key.toUpperCase()); + + return parts.join('+'); +} + +/** + * Common keyboard shortcuts used across the app + */ +export const COMMON_SHORTCUTS = { + NEW_NOTE: { key: 'n', ctrl: true, description: 'Nouvelle note' }, + SEARCH: { key: 'k', ctrl: true, description: 'Rechercher' }, + QUICK_CAPTURE: { key: 'q', ctrl: true, description: 'Capture rapide' }, + TOGGLE_SIDEBAR: { key: 'b', ctrl: true, description: 'Basculer sidebar' }, + SAVE: { key: 's', ctrl: true, description: 'Sauvegarder' }, + CLOSE: { key: 'Escape', description: 'Fermer' }, + HELP: { key: '?', shift: true, description: 'Aide raccourcis' }, + ARCHIVE: { key: 'e', ctrl: true, description: 'Archiver note' }, + DELETE: { key: 'Delete', ctrl: true, description: 'Supprimer' }, + NEXT: { key: 'j', description: 'Suivant' }, + PREVIOUS: { key: 'k', description: 'Précédent' }, + TOGGLE_TODO: { key: 'Enter', ctrl: true, description: 'Toggle todo' }, + BOLD: { key: 'b', ctrl: true, description: 'Gras' }, + ITALIC: { key: 'i', ctrl: true, description: 'Italique' }, + LINK: { key: 'k', ctrl: true, shift: true, description: 'Lien' }, +}; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index da147be..f6b22a9 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -41,6 +41,9 @@ import { PomodoroTimer } from "@/components/PomodoroTimer"; import { KanbanBoard } from "@/components/KanbanBoard"; import { getSmartTagSuggestions } from "@/utils/smartTags"; import { QuickCaptureWidget } from "@/components/QuickCaptureWidget"; +import { StatsDashboard } from "@/components/StatsDashboard"; +import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp"; +import { useKeyboardShortcuts, type KeyboardShortcut } from "@/hooks/useKeyboardShortcuts"; // ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS ===== @@ -170,6 +173,8 @@ const Index = () => { const [selectedImage, setSelectedImage] = useState(null); const [addEventModal, setAddEventModal] = useState(false); const [editEventModal, setEditEventModal] = useState<{ open: boolean, event?: CalendarEvent }>({ open: false }); + const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); + const [showStatsDashboard, setShowStatsDashboard] = useState(false); // Pagination states const [notesPage, setNotesPage] = useState(0); @@ -934,6 +939,72 @@ const Index = () => { setCalendarPage(0); }, [calendarEvents.length]); + // Define keyboard shortcuts + const keyboardShortcuts: KeyboardShortcut[] = [ + { + key: 'n', + ctrl: true, + description: 'Nouvelle note', + handler: () => !showAdmin && handleCreateNote() + }, + { + key: 'k', + ctrl: true, + description: 'Rechercher', + handler: () => { + const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement; + if (searchInput) searchInput.focus(); + } + }, + { + key: 'q', + ctrl: true, + description: 'Capture rapide', + handler: () => { + const captureButton = document.querySelector('[title="Capture rapide (Ctrl+Q)"]') as HTMLButtonElement; + if (captureButton) captureButton.click(); + } + }, + { + key: 'Escape', + description: 'Fermer note/modal', + handler: () => { + if (openNote) setOpenNote(null); + else if (showAdmin) setShowAdmin(false); + else if (showKeyboardHelp) setShowKeyboardHelp(false); + }, + preventDefault: false + }, + { + key: '?', + shift: true, + description: 'Aide raccourcis', + handler: () => setShowKeyboardHelp(true) + }, + { + key: 'e', + ctrl: true, + description: 'Archiver note', + handler: async () => { + if (openNote?.id) { + await handleUpdateNote({ archived: !openNote.archived }); + } + } + }, + { + key: 's', + ctrl: true, + description: 'Sauvegarder (auto)', + handler: () => { + // Note: Auto-save is handled by the editor + showSuccess('Auto-save actif'); + } + } + ]; + + // Register keyboard shortcuts + useKeyboardShortcuts(keyboardShortcuts, !showAdmin); + // Pagination for todos const activeTodos = todos.filter(t => !t.completed); const completedTodos = todos.filter(t => t.completed); @@ -1191,6 +1262,9 @@ const Index = () => { )} + + {/* Statistics Dashboard */} + {/* Middle Column: Notes or Open Note */} @@ -3068,6 +3142,13 @@ const Index = () => { )} + {/* Keyboard Shortcuts Help Modal */} + + {/* Quick Capture Widget */}