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;
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 [timeLeft, setTimeLeft] = useState(TIMER_DURATIONS.work);
const [isRunning, setIsRunning] = useState(false);
const [sessionsCompleted, setSessionsCompleted] = useState(0);
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(() => {
// Create audio element for notification
audioRef.current = new Audio('data:audio/wav;base64,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');
}, []);
if (onStateChange) {
onStateChange(isRunning, timeLeft, mode);
}
}, [isRunning, timeLeft, mode, onStateChange]);
useEffect(() => {
if (isRunning && timeLeft > 0) {
@@ -52,9 +99,7 @@ export function PomodoroTimer() {
setIsRunning(false);
// Play notification sound
if (audioRef.current) {
audioRef.current.play().catch(e => console.error('Audio play failed:', e));
}
playNotificationSound();
if (mode === 'work') {
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 {
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";
import AuthService from "@/services/AuthService";
import AdminService from "@/services/AdminService";
@@ -37,11 +37,12 @@ import { downloadNoteAsMarkdown, downloadAllNotesAsMarkdown, downloadTodosAsMark
import { TemplateSelector } from "@/components/TemplateSelector";
import type { NoteTemplate } from "@/utils/noteTemplates";
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 { getSmartTagSuggestions } from "@/utils/smartTags";
import { QuickCaptureWidget } from "@/components/QuickCaptureWidget";
import { StatsDashboard } from "@/components/StatsDashboard";
import { StatsModal } from "@/components/StatsModal";
import { KeyboardShortcutsHelp } from "@/components/KeyboardShortcutsHelp";
import { useKeyboardShortcuts, type KeyboardShortcut } from "@/hooks/useKeyboardShortcuts";
@@ -119,6 +120,23 @@ interface UserType {
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 [user, setUser] = useState<UserType | null>(null);
const [showAdmin, setShowAdmin] = useState(false);
@@ -175,6 +193,12 @@ const Index = () => {
const [editEventModal, setEditEventModal] = useState<{ open: boolean, event?: CalendarEvent }>({ open: false });
const [showKeyboardHelp, setShowKeyboardHelp] = 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
const [notesPage, setNotesPage] = useState(0);
@@ -253,6 +277,20 @@ const Index = () => {
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 () => {
try {
const fetchedNotes = await NotesService.getNotes();
@@ -1093,6 +1131,30 @@ const Index = () => {
Nouvelle Note
</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 />
{user && (
@@ -1262,9 +1324,6 @@ const Index = () => {
)}
</CardContent>
</Card>
{/* Statistics Dashboard */}
<StatsDashboard notes={notes} todos={todos} />
</div>
{/* Middle Column: Notes or Open Note */}
@@ -1391,7 +1450,7 @@ const Index = () => {
{note.title || "Sans titre"}
</h3>
<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>
<div className="flex gap-2 flex-wrap mb-2">
{note.todos && note.todos.length > 0 && (
@@ -1906,11 +1965,8 @@ const Index = () => {
)}
</div>
{/* Right Column: Pomodoro Timer, Todos & RSS */}
{/* Right Column: Todos & RSS */}
<div className="space-y-6">
{/* Pomodoro Timer */}
<PomodoroTimer />
{/* Todos & RSS Boxes side by side */}
<div className="grid grid-cols-2 gap-6">
{/* Todos Box */}
@@ -3149,6 +3205,21 @@ const Index = () => {
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 */}
<QuickCaptureWidget onNoteCaptured={loadNotes} />
</div>