Merge pull request #141 from R0m1k3/claude/statistics-modal-popup-01P7c6z7G5KXo9Tk6BwEa4ew

Claude/statistics modal popup 01 p7c6z7 g5 k xo9 tk6 bw ea4ew
This commit is contained in:
LogiFlow authored and GitHub committed 2025-11-28 21:35:42 +01:00
commit 6dd83c947e
4 files changed
+193 -18

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { PomodoroTimer } from "./PomodoroTimer";
interface PomodoroModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onTimerStateChange?: (isRunning: boolean, timeLeft: number, mode: string) => void;
}
export function PomodoroModal({ open, onOpenChange, onTimerStateChange }: PomodoroModalProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-2xl">Pomodoro Timer</DialogTitle>
</DialogHeader>
<div className="mt-4">
<PomodoroTimer onStateChange={onTimerStateChange} />
</div>
</DialogContent>
</Dialog>
);
}
+53 -8
View File
@@ -15,18 +15,65 @@ const TIMER_DURATIONS = {
const SESSIONS_BEFORE_LONG_BREAK = 4; const SESSIONS_BEFORE_LONG_BREAK = 4;
export function PomodoroTimer() { interface PomodoroTimerProps {
onStateChange?: (isRunning: boolean, timeLeft: number, mode: TimerMode) => void;
}
export function PomodoroTimer({ onStateChange }: PomodoroTimerProps = {}) {
const [mode, setMode] = useState<TimerMode>('work'); const [mode, setMode] = useState<TimerMode>('work');
const [timeLeft, setTimeLeft] = useState(TIMER_DURATIONS.work); const [timeLeft, setTimeLeft] = useState(TIMER_DURATIONS.work);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [sessionsCompleted, setSessionsCompleted] = useState(0); const [sessionsCompleted, setSessionsCompleted] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
// Function to play notification sound using Web Audio API
const playNotificationSound = () => {
try {
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Create a pleasant notification sound (two beeps)
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.2);
// Second beep
setTimeout(() => {
const oscillator2 = audioContext.createOscillator();
const gainNode2 = audioContext.createGain();
oscillator2.connect(gainNode2);
gainNode2.connect(audioContext.destination);
oscillator2.frequency.value = 1000;
oscillator2.type = 'sine';
gainNode2.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
oscillator2.start(audioContext.currentTime);
oscillator2.stop(audioContext.currentTime + 0.2);
}, 250);
} catch (e) {
console.error('Audio play failed:', e);
}
};
// Notify parent component of state changes
useEffect(() => { useEffect(() => {
// Create audio element for notification if (onStateChange) {
audioRef.current = new Audio('data:audio/wav;base64,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'); onStateChange(isRunning, timeLeft, mode);
}, []); }
}, [isRunning, timeLeft, mode, onStateChange]);
useEffect(() => { useEffect(() => {
if (isRunning && timeLeft > 0) { if (isRunning && timeLeft > 0) {
@@ -52,9 +99,7 @@ export function PomodoroTimer() {
setIsRunning(false); setIsRunning(false);
// Play notification sound // Play notification sound
if (audioRef.current) { playNotificationSound();
audioRef.current.play().catch(e => console.error('Audio play failed:', e));
}
if (mode === 'work') { if (mode === 'work') {
const newSessionCount = sessionsCompleted + 1; const newSessionCount = sessionsCompleted + 1;
+31
View File
@@ -0,0 +1,31 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { StatsDashboard } from "./StatsDashboard";
import { Note } from "@/services/NotesService";
import { Todo } from "@/services/TodosService";
interface StatsModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
notes: Note[];
todos: Todo[];
}
export function StatsModal({ open, onOpenChange, notes, todos }: StatsModalProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl">Statistiques</DialogTitle>
</DialogHeader>
<div className="mt-4">
<StatsDashboard notes={notes} todos={todos} />
</div>
</DialogContent>
</Dialog>
);
}
+81 -10
View File
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { RichTextEditor } from "@/components/RichTextEditor"; import { RichTextEditor } from "@/components/RichTextEditor";
import { import {
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send, Check, ChevronsUpDown, Star, Activity, FileDown, LayoutGrid, List, Sparkles Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send, Check, ChevronsUpDown, Star, Activity, FileDown, LayoutGrid, List, Sparkles, BarChart3
} from "lucide-react"; } from "lucide-react";
import AuthService from "@/services/AuthService"; import AuthService from "@/services/AuthService";
import AdminService from "@/services/AdminService"; import AdminService from "@/services/AdminService";
@@ -37,11 +37,12 @@ import { downloadNoteAsMarkdown, downloadAllNotesAsMarkdown, downloadTodosAsMark
import { TemplateSelector } from "@/components/TemplateSelector"; import { TemplateSelector } from "@/components/TemplateSelector";
import type { NoteTemplate } from "@/utils/noteTemplates"; import type { NoteTemplate } from "@/utils/noteTemplates";
import { AdvancedSearch, type SearchFilters, type TagOption } from "@/components/AdvancedSearch"; import { AdvancedSearch, type SearchFilters, type TagOption } from "@/components/AdvancedSearch";
import { PomodoroTimer } from "@/components/PomodoroTimer"; import { PomodoroModal } from "@/components/PomodoroModal";
import { KanbanBoard } from "@/components/KanbanBoard"; import { KanbanBoard } from "@/components/KanbanBoard";
import { getSmartTagSuggestions } from "@/utils/smartTags"; import { getSmartTagSuggestions } from "@/utils/smartTags";
import { QuickCaptureWidget } from "@/components/QuickCaptureWidget"; import { QuickCaptureWidget } from "@/components/QuickCaptureWidget";
import { StatsDashboard } from "@/components/StatsDashboard"; import { StatsDashboard } from "@/components/StatsDashboard";
import { StatsModal } from "@/components/StatsModal";
import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp"; import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp";
import { useKeyboardShortcuts, type KeyboardShortcut } from "@/hooks/useKeyboardShortcuts"; import { useKeyboardShortcuts, type KeyboardShortcut } from "@/hooks/useKeyboardShortcuts";
@@ -119,6 +120,23 @@ interface UserType {
is_admin: boolean; is_admin: boolean;
} }
// Function to clean HTML content and decode entities
const cleanHtmlContent = (html: string): string => {
if (!html) return '';
// Create a temporary div to decode HTML entities
const temp = document.createElement('div');
temp.innerHTML = html;
// Get text content (removes HTML tags and decodes entities)
let text = temp.textContent || temp.innerText || '';
// Remove any remaining HTML entity artifacts
text = text.replace(/&[a-z]+;?/gi, ' ').replace(/\s+/g, ' ').trim();
return text;
};
const Index = () => { const Index = () => {
const [user, setUser] = useState<UserType | null>(null); const [user, setUser] = useState<UserType | null>(null);
const [showAdmin, setShowAdmin] = useState(false); const [showAdmin, setShowAdmin] = useState(false);
@@ -175,6 +193,12 @@ const Index = () => {
const [editEventModal, setEditEventModal] = useState<{ open: boolean, event?: CalendarEvent }>({ open: false }); const [editEventModal, setEditEventModal] = useState<{ open: boolean, event?: CalendarEvent }>({ open: false });
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
const [showStatsDashboard, setShowStatsDashboard] = useState(false); const [showStatsDashboard, setShowStatsDashboard] = useState(false);
const [showPomodoroModal, setShowPomodoroModal] = useState(false);
// Pomodoro timer states
const [pomodoroRunning, setPomodoroRunning] = useState(false);
const [pomodoroTimeLeft, setPomodoroTimeLeft] = useState(25 * 60);
const [pomodoroMode, setPomodoroMode] = useState<'work' | 'shortBreak' | 'longBreak'>('work');
// Pagination states // Pagination states
const [notesPage, setNotesPage] = useState(0); const [notesPage, setNotesPage] = useState(0);
@@ -253,6 +277,20 @@ const Index = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
// Handle Pomodoro timer state changes
const handlePomodoroStateChange = (isRunning: boolean, timeLeft: number, mode: 'work' | 'shortBreak' | 'longBreak') => {
setPomodoroRunning(isRunning);
setPomodoroTimeLeft(timeLeft);
setPomodoroMode(mode);
};
// Format time for Pomodoro display
const formatPomodoroTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const loadNotes = async () => { const loadNotes = async () => {
try { try {
const fetchedNotes = await NotesService.getNotes(); const fetchedNotes = await NotesService.getNotes();
@@ -1093,6 +1131,30 @@ const Index = () => {
Nouvelle Note Nouvelle Note
</Button> </Button>
<Button
variant="ghost"
size="icon"
onClick={() => setShowStatsDashboard(true)}
title="Statistiques"
>
<BarChart3 className="h-5 w-5" />
</Button>
<Button
variant={pomodoroRunning ? "default" : "ghost"}
size="icon"
onClick={() => setShowPomodoroModal(true)}
title="Pomodoro Timer"
className="relative"
>
<Clock className="h-5 w-5" />
{pomodoroRunning && (
<span className="absolute -bottom-1 -right-1 text-[10px] font-mono bg-primary text-primary-foreground px-1 rounded">
{formatPomodoroTime(pomodoroTimeLeft)}
</span>
)}
</Button>
<ModeToggle /> <ModeToggle />
{user && ( {user && (
@@ -1262,9 +1324,6 @@ const Index = () => {
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* Statistics Dashboard */}
<StatsDashboard notes={notes} todos={todos} />
</div> </div>
{/* Middle Column: Notes or Open Note */} {/* Middle Column: Notes or Open Note */}
@@ -1391,7 +1450,7 @@ const Index = () => {
{note.title || "Sans titre"} {note.title || "Sans titre"}
</h3> </h3>
<p className="text-sm text-muted-foreground line-clamp-2 mb-3"> <p className="text-sm text-muted-foreground line-clamp-2 mb-3">
{note.content ? note.content.replace(/<[^>]*>/g, '') : "Note vide"} {note.content ? cleanHtmlContent(note.content) : "Note vide"}
</p> </p>
<div className="flex gap-2 flex-wrap mb-2"> <div className="flex gap-2 flex-wrap mb-2">
{note.todos && note.todos.length > 0 && ( {note.todos && note.todos.length > 0 && (
@@ -1906,11 +1965,8 @@ const Index = () => {
)} )}
</div> </div>
{/* Right Column: Pomodoro Timer, Todos & RSS */} {/* Right Column: Todos & RSS */}
<div className="space-y-6"> <div className="space-y-6">
{/* Pomodoro Timer */}
<PomodoroTimer />
{/* Todos & RSS Boxes side by side */} {/* Todos & RSS Boxes side by side */}
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
{/* Todos Box */} {/* Todos Box */}
@@ -3149,6 +3205,21 @@ const Index = () => {
shortcuts={keyboardShortcuts} shortcuts={keyboardShortcuts}
/> />
{/* Statistics Modal */}
<StatsModal
open={showStatsDashboard}
onOpenChange={setShowStatsDashboard}
notes={notes}
todos={todos}
/>
{/* Pomodoro Timer Modal */}
<PomodoroModal
open={showPomodoroModal}
onOpenChange={setShowPomodoroModal}
onTimerStateChange={handlePomodoroStateChange}
/>
{/* Quick Capture Widget */} {/* Quick Capture Widget */}
<QuickCaptureWidget onNoteCaptured={loadNotes} /> <QuickCaptureWidget onNoteCaptured={loadNotes} />
</div> </div>