From ff04093f21cfa33ea27a44f88e4f5b761641eae2 Mon Sep 17 00:00:00 2001 From: ghaddaditw <40211818-ghaddaditw@users.noreply.replit.com> Date: Sun, 8 Jun 2025 07:34:46 +0000 Subject: [PATCH] Add feature to help users concentrate by reducing distractions Implement Focus Mode with customizable timers, ambient sounds, and distraction blocking. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/78d8bd21-4c82-4da0-9a7c-bd83af57beb4.jpg --- client/src/components/focus/FocusButton.tsx | 40 ++ client/src/components/focus/FocusMode.tsx | 474 ++++++++++++++++++++ 2 files changed, 514 insertions(+) create mode 100644 client/src/components/focus/FocusButton.tsx create mode 100644 client/src/components/focus/FocusMode.tsx diff --git a/client/src/components/focus/FocusButton.tsx b/client/src/components/focus/FocusButton.tsx new file mode 100644 index 0000000..3143b31 --- /dev/null +++ b/client/src/components/focus/FocusButton.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Target } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import FocusMode from "./FocusMode"; + +interface FocusButtonProps { + variant?: "default" | "outline" | "secondary" | "ghost"; + size?: "default" | "sm" | "lg" | "icon"; + className?: string; +} + +export default function FocusButton({ + variant = "outline", + size = "default", + className = "" +}: FocusButtonProps) { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + setIsOpen(false)} + /> + + ); +} \ No newline at end of file diff --git a/client/src/components/focus/FocusMode.tsx b/client/src/components/focus/FocusMode.tsx new file mode 100644 index 0000000..ba9317e --- /dev/null +++ b/client/src/components/focus/FocusMode.tsx @@ -0,0 +1,474 @@ +import { useState, useEffect, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { Slider } from "@/components/ui/slider"; +import { + Play, + Pause, + Square, + Volume2, + VolumeX, + Eye, + EyeOff, + Timer, + Coffee, + Target, + Zap +} from "lucide-react"; +import { useTranslation } from "react-i18next"; + +interface FocusSession { + id: string; + duration: number; + completed: boolean; + startTime: Date; + endTime?: Date; + type: 'work' | 'break'; +} + +interface FocusModeProps { + isOpen: boolean; + onClose: () => void; +} + +const FOCUS_DURATIONS = { + pomodoro: { work: 25, shortBreak: 5, longBreak: 15 }, + custom: { work: 30, shortBreak: 10, longBreak: 20 }, + deep: { work: 90, shortBreak: 20, longBreak: 30 } +}; + +const AMBIENT_SOUNDS = [ + { id: 'rain', name: 'Rain', file: '/sounds/rain.mp3' }, + { id: 'forest', name: 'Forest', file: '/sounds/forest.mp3' }, + { id: 'cafe', name: 'Café', file: '/sounds/cafe.mp3' }, + { id: 'whitenoise', name: 'White Noise', file: '/sounds/whitenoise.mp3' }, + { id: 'ocean', name: 'Ocean Waves', file: '/sounds/ocean.mp3' } +]; + +export default function FocusMode({ isOpen, onClose }: FocusModeProps) { + const { t } = useTranslation(); + const [isActive, setIsActive] = useState(false); + const [timeLeft, setTimeLeft] = useState(25 * 60); // 25 minutes in seconds + const [currentSession, setCurrentSession] = useState<'work' | 'break'>('work'); + const [sessionCount, setSessionCount] = useState(0); + const [focusType, setFocusType] = useState('pomodoro'); + const [ambientSound, setAmbientSound] = useState(''); + const [ambientVolume, setAmbientVolume] = useState([50]); + const [blockDistractions, setBlockDistractions] = useState(true); + const [showNotifications, setShowNotifications] = useState(true); + const [sessions, setSessions] = useState([]); + + const intervalRef = useRef(); + const audioRef = useRef(null); + const startTimeRef = useRef(); + + useEffect(() => { + if (isActive && timeLeft > 0) { + intervalRef.current = setInterval(() => { + setTimeLeft(prev => { + if (prev <= 1) { + handleSessionComplete(); + return 0; + } + return prev - 1; + }); + }, 1000); + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isActive, timeLeft]); + + useEffect(() => { + if (ambientSound && audioRef.current) { + audioRef.current.volume = ambientVolume[0] / 100; + if (isActive) { + audioRef.current.play().catch(console.error); + } else { + audioRef.current.pause(); + } + } + }, [ambientSound, ambientVolume, isActive]); + + useEffect(() => { + // Apply focus mode styles when active + if (isActive && blockDistractions) { + document.body.classList.add('focus-mode-active'); + + // Hide distracting elements + const distractingElements = document.querySelectorAll('.floating-voice-control, .notification-toast'); + distractingElements.forEach(el => { + (el as HTMLElement).style.display = 'none'; + }); + } else { + document.body.classList.remove('focus-mode-active'); + + // Restore distracting elements + const distractingElements = document.querySelectorAll('.floating-voice-control, .notification-toast'); + distractingElements.forEach(el => { + (el as HTMLElement).style.display = ''; + }); + } + + return () => { + document.body.classList.remove('focus-mode-active'); + const distractingElements = document.querySelectorAll('.floating-voice-control, .notification-toast'); + distractingElements.forEach(el => { + (el as HTMLElement).style.display = ''; + }); + }; + }, [isActive, blockDistractions]); + + const handleSessionComplete = () => { + const session: FocusSession = { + id: Date.now().toString(), + duration: getDuration(currentSession), + completed: true, + startTime: startTimeRef.current || new Date(), + endTime: new Date(), + type: currentSession + }; + + setSessions(prev => [...prev, session]); + + if (showNotifications && 'Notification' in window) { + new Notification( + currentSession === 'work' + ? t('focus.sessionComplete', 'Work session complete!') + : t('focus.breakComplete', 'Break time over!') + ); + } + + // Auto-switch between work and break + if (currentSession === 'work') { + setSessionCount(prev => prev + 1); + const isLongBreak = (sessionCount + 1) % 4 === 0; + setCurrentSession('break'); + setTimeLeft(getDuration(isLongBreak ? 'longBreak' : 'shortBreak')); + } else { + setCurrentSession('work'); + setTimeLeft(getDuration('work')); + } + + setIsActive(false); + }; + + const getDuration = (type: 'work' | 'shortBreak' | 'longBreak' | 'break') => { + if (type === 'break') type = 'shortBreak'; + const durations = FOCUS_DURATIONS[focusType]; + return durations[type] * 60; // Convert to seconds + }; + + const startSession = () => { + setIsActive(true); + startTimeRef.current = new Date(); + + // Request notification permission + if (showNotifications && 'Notification' in window && Notification.permission === 'default') { + Notification.requestPermission(); + } + }; + + const pauseSession = () => { + setIsActive(false); + }; + + const stopSession = () => { + setIsActive(false); + setTimeLeft(getDuration('work')); + setCurrentSession('work'); + setSessionCount(0); + }; + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + const getProgress = () => { + const totalDuration = getDuration(currentSession); + return ((totalDuration - timeLeft) / totalDuration) * 100; + }; + + const getTodaysStats = () => { + const today = new Date().toDateString(); + const todaySessions = sessions.filter(s => s.startTime.toDateString() === today); + const workSessions = todaySessions.filter(s => s.type === 'work' && s.completed); + const totalTime = workSessions.reduce((acc, s) => acc + s.duration, 0); + + return { + sessions: workSessions.length, + totalTime: Math.round(totalTime / 60), // Convert to minutes + streak: getStreak() + }; + }; + + const getStreak = () => { + // Calculate consecutive days with at least one completed work session + let streak = 0; + const today = new Date(); + + for (let i = 0; i < 30; i++) { + const checkDate = new Date(today); + checkDate.setDate(today.getDate() - i); + const dateString = checkDate.toDateString(); + + const hasSession = sessions.some(s => + s.startTime.toDateString() === dateString && + s.type === 'work' && + s.completed + ); + + if (hasSession) { + streak++; + } else if (i > 0) { + break; + } + } + + return streak; + }; + + const stats = getTodaysStats(); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+ +

{t('focus.title', 'Focus Mode')}

+
+ +
+ +
+ {/* Main Timer */} +
+ + + + {currentSession === 'work' ? ( + <> + + {t('focus.workSession', 'Work Session')} + + ) : ( + <> + + {t('focus.breakTime', 'Break Time')} + + )} + + {t('focus.session', 'Session')} {sessionCount + 1} + + + + + {/* Timer Display */} +
+ {formatTime(timeLeft)} +
+ + {/* Progress Bar */} +
+ +
+ + {/* Controls */} +
+ {!isActive ? ( + + ) : ( + + )} + +
+ + {/* Session Type Selector */} +
+ + +
+
+
+
+ + {/* Settings & Stats */} +
+ {/* Today's Stats */} + + + + + {t('focus.todaysStats', "Today's Stats")} + + + +
+
+
{stats.sessions}
+
{t('focus.sessions', 'Sessions')}
+
+
+
{stats.totalTime}m
+
{t('focus.focused', 'Focused')}
+
+
+
{stats.streak}
+
{t('focus.streak', 'Streak')}
+
+
+
+
+ + {/* Settings */} + + + {t('focus.settings', 'Settings')} + + + {/* Block Distractions */} +
+ + +
+ + {/* Notifications */} +
+ + +
+ + {/* Ambient Sound */} +
+ + + + {ambientSound && ( +
+
+ + +
+ +
+ )} +
+
+
+
+
+
+ + {/* Audio Element */} + {ambientSound && ( +
+ ); +} \ No newline at end of file