mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
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:
4 files changed
+193
-18
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user