Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ZenPomo Pro - Ultimate Productivity Timer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Custom CSS (for parts not covered by Tailwind) */ | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.6; } | |
| 25% { transform: translate(15px, 10px) scale(1.1); opacity: 0.8; } | |
| 50% { transform: translate(-10px, 15px) scale(0.9); opacity: 0.5; } | |
| 75% { transform: translate(5px, -10px) scale(1.05); opacity: 0.7; } | |
| } | |
| .timer-animation .circle { | |
| animation: float 15s infinite ease-in-out; | |
| background: rgba(108, 92, 231, 0.05); | |
| } | |
| .progress-bar { | |
| transition: width 0.5s ease, background-color 0.5s ease; | |
| } | |
| .task-item:hover .task-actions { | |
| opacity: 1; | |
| } | |
| .modal { | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: opacity 0.3s ease, visibility 0s 0.3s linear; | |
| } | |
| .modal.open { | |
| opacity: 1; | |
| visibility: visible; | |
| transition: opacity 0.3s ease, visibility 0s linear; | |
| } | |
| .modal-content { | |
| transform: translateY(20px) scale(0.95); | |
| transition: transform 0.3s ease; | |
| } | |
| .modal.open .modal-content { | |
| transform: translateY(0) scale(1); | |
| } | |
| .notification { | |
| transform: translateX(calc(100% + 20px)); | |
| opacity: 0; | |
| transition: transform 0.4s ease, opacity 0.4s ease; | |
| } | |
| .notification.show { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| .settings-content { | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.5s ease; | |
| } | |
| .settings-content.open { | |
| max-height: 600px; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| /* Custom task scrollbar */ | |
| .task-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .task-list::-webkit-scrollbar-track { | |
| background: #e9ecef; | |
| border-radius: 10px; | |
| } | |
| .task-list::-webkit-scrollbar-thumb { | |
| background: #a29bfe; | |
| border-radius: 10px; | |
| } | |
| .task-list::-webkit-scrollbar-thumb:hover { | |
| background: #6c5ce7; | |
| } | |
| /* Disable selection on timer and buttons */ | |
| .timer-display, button { | |
| user-select: none; | |
| -webkit-user-select: none; | |
| -moz-user-select: none; | |
| -ms-user-select: none; | |
| } | |
| /* Sound wave animation when timer running */ | |
| .sound-wave { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 3px; | |
| height: 30px; | |
| } | |
| .sound-wave div { | |
| width: 3px; | |
| height: 10px; | |
| background-color: #6c5ce7; | |
| animation: sound-wave 1.5s infinite ease-in-out; | |
| } | |
| .sound-wave div:nth-child(1) { animation-delay: 0s; } | |
| .sound-wave div:nth-child(2) { animation-delay: 0.2s; } | |
| .sound-wave div:nth-child(3) { animation-delay: 0.4s; } | |
| .sound-wave div:nth-child(4) { animation-delay: 0.6s; } | |
| .sound-wave div:nth-child(5) { animation-delay: 0.8s; } | |
| @keyframes sound-wave { | |
| 0%, 100% { height: 10px; background-color: #6c5ce7; } | |
| 25% { height: 30px; background-color: #a29bfe; } | |
| 50% { height: 15px; background-color: #5649c5; } | |
| 75% { height: 25px; background-color: #8476f9; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-gray-50 to-gray-200 min-h-screen flex flex-col text-gray-800"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-md py-4 px-6 sticky top-0 z-50 flex flex-col sm:flex-row items-center justify-between gap-4"> | |
| <a href="#" class="flex items-center gap-3 text-2xl font-bold text-purple-600 no-underline"> | |
| <i class="fas fa-clock text-3xl"></i> | |
| <span>ZenPomo Pro</span> | |
| </a> | |
| <nav class="flex-1"> | |
| <ul class="flex flex-wrap justify-center gap-2 sm:gap-4"> | |
| <li><a href="#" class="flex items-center gap-1.5 px-3 py-2 rounded-full transition-all bg-purple-100 text-purple-600"><i class="fas fa-home"></i> <span>Home</span></a></li> | |
| <li><a href="#" id="nav-stats" class="flex items-center gap-1.5 px-3 py-2 rounded-full transition-all hover:bg-gray-100 hover:text-purple-600"><i class="fas fa-chart-line"></i> <span>Stats</span></a></li> | |
| <li><a href="#" id="nav-tasks" class="flex items-center gap-1.5 px-3 py-2 rounded-full transition-all hover:bg-gray-100 hover:text-purple-600"><i class="fas fa-tasks"></i> <span>Tasks</span></a></li> | |
| <li><a href="#" id="nav-settings" class="flex items-center gap-1.5 px-3 py-2 rounded-full transition-all hover:bg-gray-100 hover:text-purple-600"><i class="fas fa-cog"></i> <span>Settings</span></a></li> | |
| </ul> | |
| </nav> | |
| <div class="flex gap-3"> | |
| <button class="flex items-center gap-2 px-4 py-2 rounded-full border-2 border-purple-600 text-purple-600 font-semibold transition-all hover:bg-purple-50 hover:shadow-sm"> | |
| <i class="fas fa-user"></i> | |
| <span>Sign In</span> | |
| </button> | |
| <button class="flex items-center gap-2 px-4 py-2 rounded-full bg-purple-600 text-white font-semibold transition-all hover:bg-purple-700 hover:shadow-md"> | |
| <i class="fas fa-rocket"></i> | |
| <span>Upgrade</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-1 p-4 max-w-7xl mx-auto w-full"> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <!-- Pomodoro Timer Card --> | |
| <div class="bg-white rounded-2xl shadow-lg p-6 relative overflow-hidden"> | |
| <!-- Decorative background circles --> | |
| <div class="timer-animation absolute inset-0 pointer-events-none overflow-hidden"> | |
| <div class="circle absolute rounded-full" style="width: 100px; height: 100px; top: 10%; left: 20%; animation-delay: 0s;"></div> | |
| <div class="circle absolute rounded-full" style="width: 150px; height: 150px; top: 60%; left: 70%; animation-delay: 2s;"></div> | |
| <div class="circle absolute rounded-full" style="width: 80px; height: 80px; top: 30%; left: 50%; animation-delay: 4s;"></div> | |
| </div> | |
| <!-- Card Header --> | |
| <header class="flex justify-between items-center mb-6 relative z-10"> | |
| <h2 class="text-xl font-bold text-purple-600 flex items-center gap-2"> | |
| <i class="fas fa-stopwatch"></i> | |
| <span>Pomodoro Timer</span> | |
| </h2> | |
| <div class="flex gap-2"> | |
| <button id="stats-btn" class="w-9 h-9 rounded-full flex items-center justify-center border-2 border-purple-600 text-purple-600 hover:bg-purple-100 transition-all" title="View Stats"> | |
| <i class="fas fa-chart-pie"></i> | |
| </button> | |
| <button id="fullscreen-btn" class="w-9 h-9 rounded-full flex items-center justify-center border-2 border-purple-600 text-purple-600 hover:bg-purple-100 transition-all" title="Fullscreen"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Mode Indicator --> | |
| <div class="flex justify-center mb-6 relative z-10"> | |
| <div id="mode-text" class="px-5 py-3 rounded-full font-semibold flex items-center gap-2 bg-purple-100 text-purple-600"> | |
| <i class="fas fa-bolt"></i> | |
| <span>Focus</span> | |
| </div> | |
| </div> | |
| <!-- Timer Display --> | |
| <div id="timer" class="text-7xl md:text-8xl font-bold text-center my-8 text-purple-600 font-mono transition-colors">25:00</div> | |
| <!-- Progress Bar --> | |
| <div class="w-full h-2.5 bg-gray-200 rounded-full mb-8 overflow-hidden"> | |
| <div id="progress-bar" class="h-full bg-purple-600 w-0"></div> | |
| </div> | |
| <!-- Controls --> | |
| <div class="flex flex-wrap justify-center gap-3 mb-8 relative z-10"> | |
| <button id="start-btn" class="px-6 py-3 rounded-full bg-purple-600 text-white font-semibold flex items-center gap-2 hover:bg-purple-700 hover:shadow-md transition-all"> | |
| <i class="fas fa-play"></i> | |
| <span>Start</span> | |
| </button> | |
| <button id="pause-btn" disabled class="px-6 py-3 rounded-full bg-green-500 text-white font-semibold flex items-center gap-2 hover:bg-green-600 hover:shadow-md transition-all"> | |
| <i class="fas fa-pause"></i> | |
| <span>Pause</span> | |
| </button> | |
| <button id="reset-btn" disabled class="px-6 py-3 rounded-full bg-pink-500 text-white font-semibold flex items-center gap-2 hover:bg-pink-600 hover:shadow-md transition-all"> | |
| <i class="fas fa-redo"></i> | |
| <span>Reset</span> | |
| </button> | |
| <button id="skip-to-break-btn" class="hidden px-6 py-3 rounded-full bg-yellow-400 text-gray-800 font-semibold flex items-center gap-2 hover:bg-yellow-500 hover:shadow-md transition-all" title="Skip to next break"> | |
| <i class="fas fa-forward"></i> | |
| <span>Skip</span> | |
| </button> | |
| </div> | |
| <!-- Activity status --> | |
| <div id="activity-status" class="flex items-center justify-center gap-2 mb-4 text-purple-600"> | |
| <div class="sound-wave hidden"> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| </div> | |
| <span id="status-text" class="text-sm">Timer idle</span> | |
| </div> | |
| <!-- Settings Toggle --> | |
| <div class="flex justify-center mb-4 relative z-10"> | |
| <button id="toggle-settings" class="px-4 py-2 rounded-full border-2 border-purple-600 text-purple-600 font-medium flex items-center gap-2 hover:bg-purple-50 transition-all"> | |
| <i class="fas fa-sliders-h"></i> | |
| <span>Timer Settings</span> | |
| </button> | |
| </div> | |
| <!-- Settings Content --> | |
| <div id="settings-content" class="overflow-hidden"> | |
| <div class="space-y-4 mb-4"> | |
| <div> | |
| <label for="work-duration" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-briefcase text-purple-600"></i> | |
| <span>Work Duration (minutes)</span> | |
| </label> | |
| <input type="number" id="work-duration" min="1" max="120" value="25" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label for="break-duration" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-coffee text-pink-500"></i> | |
| <span>Short Break (minutes)</span> | |
| </label> | |
| <input type="number" id="break-duration" min="1" max="30" value="5" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| </div> | |
| <div> | |
| <label for="long-break-duration" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-umbrella-beach text-green-500"></i> | |
| <span>Long Break (minutes)</span> | |
| </label> | |
| <input type="number" id="long-break-duration" min="1" max="60" value="15" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="pomodoros-before-long-break" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-layer-group text-purple-600"></i> | |
| <span>Pomodoros Before Long Break</span> | |
| </label> | |
| <input type="number" id="pomodoros-before-long-break" min="1" max="10" value="4" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| </div> | |
| <div> | |
| <label for="auto-start" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-magic text-purple-600"></i> | |
| <span>Auto Start Next Round</span> | |
| </label> | |
| <select id="auto-start" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| <option value="false">Disabled</option> | |
| <option value="true">Enabled</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="timer-sound" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-volume-up text-purple-600"></i> | |
| <span>Timer Sound</span> | |
| </label> | |
| <select id="timer-sound" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| <option value="bell">Classic Bell</option> | |
| <option value="chime">Soft Chime</option> | |
| <option value="digital">Digital Beep</option> | |
| <option value="nature">Nature Sounds</option> | |
| <option value="none">No Sound</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="dark-mode" class="block mb-2 font-medium flex items-center gap-2"> | |
| <i class="fas fa-moon text-purple-600"></i> | |
| <span>Theme</span> | |
| </label> | |
| <select id="dark-mode" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| <option value="light">Light</option> | |
| <option value="dark">Dark</option> | |
| <option value="system">System Default</option> | |
| </select> | |
| </div> | |
| </div> | |
| <button id="apply-settings-btn" class="w-full px-4 py-3 rounded-full bg-purple-600 text-white font-semibold flex items-center justify-center gap-2 hover:bg-purple-700 hover:shadow-md transition-all"> | |
| <i class="fas fa-check"></i> | |
| <span>Apply Settings</span> | |
| </button> | |
| </div> | |
| <!-- Stats Section --> | |
| <div class="flex gap-3 mt-6 relative z-10"> | |
| <div class="bg-white rounded-xl shadow-sm p-4 text-center flex-1"> | |
| <div id="today-pomodoros" class="text-2xl font-bold text-purple-600">0</div> | |
| <div class="text-sm text-gray-500">Today</div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-sm p-4 text-center flex-1"> | |
| <div id="week-pomodoros" class="text-2xl font-bold text-purple-600">0</div> | |
| <div class="text-sm text-gray-500">This Week</div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-sm p-4 text-center flex-1"> | |
| <div id="total-pomodoros" class="text-2xl font-bold text-purple-600">0</div> | |
| <div class="text-sm text-gray-500">Total</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Task Manager Card --> | |
| <div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col h-full"> | |
| <!-- Card Header --> | |
| <header class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-bold text-purple-600 flex items-center gap-2"> | |
| <i class="fas fa-tasks"></i> | |
| <span>Task Manager</span> | |
| </h2> | |
| <div class="flex gap-2"> | |
| <button id="clear-completed" class="w-9 h-9 rounded-full flex items-center justify-center border-2 border-purple-600 text-purple-600 hover:bg-purple-100 transition-all" title="Clear Completed Tasks"> | |
| <i class="fas fa-check-double"></i> | |
| </button> | |
| <button id="add-task-modal" class="w-9 h-9 rounded-full flex items-center justify-center bg-purple-600 text-white hover:bg-purple-700 transition-all" title="Add New Task"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Quick Task Input --> | |
| <div class="flex mb-6"> | |
| <input type="text" id="new-task" placeholder="Add a quick task..." class="flex-1 px-4 py-3 rounded-l-xl border border-gray-300 focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all border-r-0"> | |
| <button id="add-task" class="px-4 py-3 rounded-r-xl bg-purple-600 text-white font-medium hover:bg-purple-700 transition-all"> | |
| <i class="fas fa-plus"></i> | |
| <span class="hidden sm:inline">Add</span> | |
| </button> | |
| </div> | |
| <!-- Task Categories --> | |
| <div class="flex flex-wrap gap-2 mb-6"> | |
| <button class="px-3 py-1.5 rounded-full font-medium transition-all bg-purple-600 text-white" data-category="all">All</button> | |
| <button class="px-3 py-1.5 rounded-full font-medium transition-all bg-gray-200 hover:bg-gray-300" data-category="work">Work</button> | |
| <button class="px-3 py-1.5 rounded-full font-medium transition-all bg-gray-200 hover:bg-gray-300" data-category="personal">Personal</button> | |
| <button class="px-3 py-1.5 rounded-full font-medium transition-all bg-gray-200 hover:bg-gray-300" data-category="study">Study</button> | |
| <button class="px-3 py-1.5 rounded-full font-medium transition-all bg-gray-200 hover:bg-gray-300" data-category="other">Other</button> | |
| </div> | |
| <!-- Task List --> | |
| <ul id="task-list" class="flex-1 overflow-y-auto max-h-96 mb-6"> | |
| <!-- Tasks will be rendered here --> | |
| </ul> | |
| <!-- Empty State --> | |
| <div id="empty-state" class="flex flex-col items-center justify-center py-12 text-center text-gray-500"> | |
| <i class="fas fa-clipboard-list text-6xl mb-4 text-gray-300"></i> | |
| <p class="max-w-xs">No tasks yet. Add your first task to get started!</p> | |
| </div> | |
| <!-- Task Stats --> | |
| <div class="flex justify-between pt-4 border-t border-gray-200 text-sm text-gray-500"> | |
| <span id="total-tasks">0 tasks</span> | |
| <span id="completed-tasks">0 completed</span> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Task Modal --> | |
| <div class="modal fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50" id="task-modal"> | |
| <div class="modal-content bg-white rounded-2xl shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto"> | |
| <header class="p-6 border-b border-gray-200 flex justify-between items-center"> | |
| <h3 id="modal-task-title" class="text-xl font-semibold text-gray-800">Add New Task</h3> | |
| <button id="close-modal" class="text-2xl text-gray-500 hover:text-gray-700 transition-all">×</button> | |
| </header> | |
| <div class="p-6 space-y-4"> | |
| <input type="hidden" id="task-id-input"> | |
| <div> | |
| <label for="task-name" class="block mb-2 font-medium text-gray-700">Task Name</label> | |
| <input type="text" id="task-name" placeholder="Enter task name" required class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label for="task-category" class="block mb-2 font-medium text-gray-700">Category</label> | |
| <select id="task-category" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| <option value="work">Work</option> | |
| <option value="personal">Personal</option> | |
| <option value="study">Study</option> | |
| <option value="other">Other</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="task-priority" class="block mb-2 font-medium text-gray-700">Priority</label> | |
| <select id="task-priority" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| <option value="low">Low</option> | |
| <option value="medium" selected>Medium</option> | |
| <option value="high">High</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="task-due-date" class="block mb-2 font-medium text-gray-700">Due Date (optional)</label> | |
| <input type="date" id="task-due-date" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"> | |
| </div> | |
| <div> | |
| <label for="task-notes" class="block mb-2 font-medium text-gray-700">Notes (optional)</label> | |
| <textarea id="task-notes" rows="3" placeholder="Add any additional notes..." class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:border-purple-600 focus:ring-2 focus:ring-purple-200 outline-none transition-all"></textarea> | |
| </div> | |
| </div> | |
| <footer class="p-6 border-t border-gray-200 flex justify-end gap-3"> | |
| <button id="cancel-task" class="px-5 py-2.5 rounded-full border-2 border-gray-300 text-gray-700 font-medium hover:bg-gray-100 transition-all">Cancel</button> | |
| <button id="save-task" class="px-5 py-2.5 rounded-full bg-purple-600 text-white font-medium hover:bg-purple-700 transition-all">Save Task</button> | |
| </footer> | |
| </div> | |
| </div> | |
| <!-- Stats Modal --> | |
| <div class="modal fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50" id="stats-modal"> | |
| <div class="modal-content bg-white rounded-2xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto"> | |
| <header class="p-6 border-b border-gray-200 flex justify-between items-center"> | |
| <h3 class="text-xl font-semibold text-gray-800 flex items-center gap-2"> | |
| <i class="fas fa-chart-pie text-purple-600"></i> | |
| <span>Productivity Stats</span> | |
| </h3> | |
| <button id="close-stats-modal" class="text-2xl text-gray-500 hover:text-gray-700 transition-all">×</button> | |
| </header> | |
| <div class="p-6"> | |
| <div class="border-b border-gray-200 mb-6"> | |
| <div class="flex gap-1"> | |
| <button class="tab px-4 py-2 font-medium relative" data-tab="history">History</button> | |
| <button class="tab px-4 py-2 font-medium relative" data-tab="summary">Summary</button> | |
| </div> | |
| </div> | |
| <div id="history-tab" class="tab-content active"> | |
| <h4 class="text-lg font-medium mb-4 text-gray-800">Recent Pomodoro Sessions</h4> | |
| <div id="history-list" class="max-h-60 overflow-y-auto"> | |
| <div id="history-empty-state" class="py-8 text-center text-gray-500"> | |
| No completed sessions yet. | |
| </div> | |
| </div> | |
| </div> | |
| <div id="summary-tab" class="tab-content"> | |
| <h4 class="text-lg font-medium mb-4 text-gray-800">Overall Summary</h4> | |
| <div class="space-y-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="stat-card bg-white rounded-xl shadow-sm p-4 text-center"> | |
| <div id="modal-today-pomodoros" class="text-2xl font-bold text-purple-600">0</div> | |
| <div class="text-sm text-gray-500">Pomodoros Today</div> | |
| </div> | |
| <div class="stat-card bg-white rounded-xl shadow-sm p-4 text-center"> | |
| <div id="modal-week-pomodoros" class="text-2xl font-bold text-purple-600">0</div> | |
| <div class="text-sm text-gray-500">Pomodoros This Week</div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="stat-card bg-white rounded-xl shadow-sm p-4 text-center"> | |
| <div id="modal-total-pomodoros" class="text-2xl font-bold text-purple-600">0</div> | |
| <div class="text-sm text-gray-500">Total Pomodoros</div> | |
| </div> | |
| <div class="stat-card bg-white rounded-xl shadow-sm p-4 text-center"> | |
| <div id="modal-total-focus-time" class="text-2xl font-bold text-purple-600">0h 0m</div> | |
| <div class="text-sm text-gray-500">Total Focus Time</div> | |
| </div> | |
| </div> | |
| <div class="stat-card bg-white rounded-xl shadow-sm p-4 text-center"> | |
| <div id="modal-current-streak" class="text-2xl font-bold text-purple-600">0 days</div> | |
| <div class="text-sm text-gray-500">Current Streak</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer class="p-6 border-t border-gray-200"> | |
| <button id="reset-stats-btn" class="w-full px-4 py-2.5 rounded-full border-2 border-red-500 text-red-500 font-medium hover:bg-red-50 flex items-center justify-center gap-2 transition-all"> | |
| <i class="fas fa-trash-alt"></i> | |
| <span>Reset All Stats</span> | |
| </button> | |
| </footer> | |
| </div> | |
| </div> | |
| <!-- Notification --> | |
| <div id="notification" class="fixed bottom-6 right-6 bg-white rounded-lg shadow-lg p-4 flex items-center gap-3 max-w-sm w-full border-l-4 hidden"> | |
| <div class="notification-icon"> | |
| <i class="fas fa-info-circle text-blue-500 text-xl"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <div id="notification-title" class="font-semibold">Info</div> | |
| <div id="notification-message" class="text-sm text-gray-600">Notification message</div> | |
| </div> | |
| <button id="close-notification" class="text-gray-500 hover:text-gray-700 transition-all">×</button> | |
| </div> | |
| <!-- Audio Elements --> | |
| <audio id="timer-end-sound" preload="auto"></audio> | |
| <audio id="break-start-sound" preload="auto"></audio> | |
| <audio id="work-start-sound" preload="auto"></audio> | |
| <audio id="task-complete-sound" preload="auto"></audio> | |
| <audio id="focus-alarm-sound" preload="auto"></audio> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // --- Constants --- | |
| const SOUND_URLS = { | |
| bell: 'https://assets.mixkit.co/sfx/preview/mixkit-alarm-digital-clock-beep-989.mp3', | |
| chime: 'https://assets.mixkit.co/sfx/preview/mixkit-positive-interface-beep-221.mp3', | |
| digital: 'https://assets.mixkit.co/sfx/preview/mixkit-correct-answer-tone-2870.mp3', | |
| nature: 'https://assets.mixkit.co/sfx/preview/mixkit-forest-birds-singing-1238.mp3', | |
| none: '' | |
| }; | |
| const PRIORITY_COLORS = { | |
| high: 'bg-red-500', | |
| medium: 'bg-yellow-400', | |
| low: 'bg-green-500' | |
| }; | |
| // --- DOM Elements --- | |
| // Timer elements | |
| const timerDisplay = document.getElementById('timer'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const pauseBtn = document.getElementById('pause-btn'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| const skipToBreakBtn = document.getElementById('skip-to-break-btn'); | |
| const modeText = document.getElementById('mode-text'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const soundWave = document.querySelector('.sound-wave'); | |
| const statusText = document.getElementById('status-text'); | |
| const fullscreenBtn = document.getElementById('fullscreen-btn'); | |
| const activityStatus = document.getElementById('activity-status'); | |
| // Settings elements | |
| const workDurationInput = document.getElementById('work-duration'); | |
| const breakDurationInput = document.getElementById('break-duration'); | |
| const longBreakDurationInput = document.getElementById('long-break-duration'); | |
| const pomodorosBeforeLongBreakInput = document.getElementById('pomodoros-before-long-break'); | |
| const autoStartInput = document.getElementById('auto-start'); | |
| const timerSoundInput = document.getElementById('timer-sound'); | |
| const darkModeInput = document.getElementById('dark-mode'); | |
| const toggleSettingsBtn = document.getElementById('toggle-settings'); | |
| const settingsContent = document.getElementById('settings-content'); | |
| const applySettingsBtn = document.getElementById('apply-settings-btn'); | |
| // Stats elements | |
| const todayPomodorosDisplay = document.getElementById('today-pomodoros'); | |
| const weekPomodorosDisplay = document.getElementById('week-pomodoros'); | |
| const totalPomodorosDisplay = document.getElementById('total-pomodoros'); | |
| // Task elements | |
| const newTaskInput = document.getElementById('new-task'); | |
| const addTaskBtn = document.getElementById('add-task'); | |
| const taskList = document.getElementById('task-list'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const totalTasksSpan = document.getElementById('total-tasks'); | |
| const completedTasksSpan = document.getElementById('completed-tasks'); | |
| const clearCompletedBtn = document.getElementById('clear-completed'); | |
| const categoryBtns = document.querySelectorAll('.category-btn'); | |
| // Task modal elements | |
| const taskModal = document.getElementById('task-modal'); | |
| const addTaskModalBtn = document.getElementById('add-task-modal'); | |
| const closeModalBtn = document.getElementById('close-modal'); | |
| const cancelTaskBtn = document.getElementById('cancel-task'); | |
| const saveTaskBtn = document.getElementById('save-task'); | |
| const modalTaskTitle = document.getElementById('modal-task-title'); | |
| const taskNameInput = document.getElementById('task-name'); | |
| const taskCategoryInput = document.getElementById('task-category'); | |
| const taskPriorityInput = document.getElementById('task-priority'); | |
| const taskDueDateInput = document.getElementById('task-due-date'); | |
| const taskNotesInput = document.getElementById('task-notes'); | |
| const taskIdInput = document.getElementById('task-id-input'); | |
| // Stats modal elements | |
| const statsModal = document.getElementById('stats-modal'); | |
| const statsBtn = document.getElementById('stats-btn'); | |
| const closeStatsModalBtn = document.getElementById('close-stats-modal'); | |
| const statsTabs = statsModal.querySelectorAll('.tab'); | |
| const statsTabContents = statsModal.querySelectorAll('.tab-content'); | |
| const historyList = document.getElementById('history-list'); | |
| const historyEmptyState = document.getElementById('history-empty-state'); | |
| const modalTodayPomodoros = document.getElementById('modal-today-pomodoros'); | |
| const modalWeekPomodoros = document.getElementById('modal-week-pomodoros'); | |
| const modalTotalPomodoros = document.getElementById('modal-total-pomodoros'); | |
| const modalTotalFocusTime = document.getElementById('modal-total-focus-time'); | |
| const modalCurrentStreak = document.getElementById('modal-current-streak'); | |
| const resetStatsBtn = document.getElementById('reset-stats-btn'); | |
| // Notification elements | |
| const notification = document.getElementById('notification'); | |
| const notificationIcon = notification.querySelector('.notification-icon i'); | |
| const notificationTitle = document.getElementById('notification-title'); | |
| const notificationMessage = document.getElementById('notification-message'); | |
| const closeNotificationBtn = document.getElementById('close-notification'); | |
| // Audio elements | |
| const timerEndSound = document.getElementById('timer-end-sound'); | |
| const breakStartSound = document.getElementById('break-start-sound'); | |
| const workStartSound = document.getElementById('work-start-sound'); | |
| const taskCompleteSound = document.getElementById('task-complete-sound'); | |
| const focusAlarmSound = document.getElementById('focus-alarm-sound'); | |
| // --- State Variables --- | |
| let timerInterval; | |
| let timeLeft; | |
| let totalSeconds; | |
| let isRunning = false; | |
| let currentMode = 'work'; | |
| let pomodoroCount = 0; | |
| let settings = loadSettings(); | |
| let tasks = loadTasks(); | |
| let stats = loadStats(); | |
| let currentTaskEditingId = null; | |
| let currentCategoryFilter = 'all'; | |
| let notificationTimeout; | |
| let isFullscreen = false; | |
| // --- Initialization --- | |
| function initializeApp() { | |
| applySettingsToUI(); | |
| updateSoundSources(); | |
| resetTimer(true); | |
| renderTasks(); | |
| updateTaskStats(); | |
| updateStatsDisplay(); | |
| updateButtonStates(); | |
| checkDarkMode(); | |
| // Initialize tooltips | |
| initializeTooltips(); | |
| // Load sounds and try to unlock audio | |
| unlockAudio(); | |
| } | |
| // --- Init Tooltips --- | |
| function initializeTooltips() { | |
| // Add event listeners for all elements with title attribute | |
| document.querySelectorAll('[title]').forEach(el => { | |
| el.addEventListener('mouseenter', showTooltip); | |
| el.addEventListener('mouseleave', hideTooltip); | |
| el.addEventListener('focus', showTooltip); | |
| el.addEventListener('blur', hideTooltip); | |
| }); | |
| } | |
| function showTooltip(e) { | |
| const element = e.target; | |
| const tooltipText = element.getAttribute('title'); | |
| if (!tooltipText) return; | |
| // Create tooltip if it doesn't exist | |
| let tooltip = element.querySelector('.custom-tooltip'); | |
| if (!tooltip) { | |
| tooltip = document.createElement('div'); | |
| tooltip.className = 'custom-tooltip absolute z-50 bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap pointer-events-none hidden'; | |
| element.appendChild(tooltip); | |
| } | |
| // Position tooltip | |
| tooltip.textContent = tooltipText; | |
| tooltip.classList.remove('hidden'); | |
| const rect = element.getBoundingClientRect(); | |
| tooltip.style.left = `${rect.left + rect.width/2 - tooltip.offsetWidth/2}px`; | |
| tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`; | |
| // Remove title to prevent default tooltip | |
| element.removeAttribute('title'); | |
| } | |
| function hideTooltip(e) { | |
| const element = e.target; | |
| const tooltip = element.querySelector('.custom-tooltip'); | |
| if (tooltip) { | |
| tooltip.classList.add('hidden'); | |
| } | |
| } | |
| // --- Settings Management --- | |
| function defaultSettings() { | |
| return { | |
| workDuration: 25, | |
| breakDuration: 5, | |
| longBreakDuration: 15, | |
| pomodorosBeforeLongBreak: 4, | |
| autoStart: false, | |
| timerSound: 'bell', | |
| darkMode: 'system' | |
| }; | |
| } | |
| function loadSettings() { | |
| const savedSettings = localStorage.getItem('zenpomoSettings'); | |
| return { ...defaultSettings(), ...(savedSettings ? JSON.parse(savedSettings) : {}) }; | |
| } | |
| function saveSettings() { | |
| localStorage.setItem('zenpomoSettings', JSON.stringify(settings)); | |
| checkDarkMode(); // Update dark mode when settings change | |
| } | |
| function applySettingsToUI() { | |
| workDurationInput.value = settings.workDuration; | |
| breakDurationInput.value = settings.breakDuration; | |
| longBreakDurationInput.value = settings.longBreakDuration; | |
| pomodorosBeforeLongBreakInput.value = settings.pomodorosBeforeLongBreak; | |
| autoStartInput.value = settings.autoStart.toString(); | |
| timerSoundInput.value = settings.timerSound; | |
| darkModeInput.value = settings.darkMode; | |
| } | |
| function checkDarkMode() { | |
| let darkMode = false; | |
| if (settings.darkMode === 'dark' || | |
| (settings.darkMode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| darkMode = true; | |
| } | |
| if (darkMode) { | |
| document.documentElement.classList.add('dark'); | |
| document.body.classList.add('bg-gray-900'); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| document.body.classList.remove('bg-gray-900'); | |
| } | |
| } | |
| function applySettings() { | |
| // Validate inputs | |
| const workDur = parseInt(workDurationInput.value); | |
| const breakDur = parseInt(breakDurationInput.value); | |
| const longBreakDur = parseInt(longBreakDurationInput.value); | |
| const pomBeforeLong = parseInt(pomodorosBeforeLongBreakInput.value); | |
| if (isNaN(workDur) || workDur < 1 || workDur > 120 || | |
| isNaN(breakDur) || breakDur < 1 || breakDur > 30 || | |
| isNaN(longBreakDur) || longBreakDur < 1 || longBreakDur > 60 || | |
| isNaN(pomBeforeLong) || pomBeforeLong < 1 || pomBeforeLong > 10) { | |
| showNotification('Invalid Settings', 'Please enter valid numbers within allowed ranges.', 'error'); | |
| return; | |
| } | |
| // Update settings | |
| settings.workDuration = workDur; | |
| settings.breakDuration = breakDur; | |
| settings.longBreakDuration = longBreakDur; | |
| settings.pomodorosBeforeLongBreak = pomBeforeLong; | |
| settings.autoStart = autoStartInput.value === 'true'; | |
| settings.timerSound = timerSoundInput.value; | |
| settings.darkMode = darkModeInput.value; | |
| saveSettings(); | |
| updateSoundSources(); | |
| if (!isRunning) { | |
| resetTimer(true); // Update timer display without changing mode | |
| } | |
| showNotification('Settings Applied', 'New settings saved successfully.', 'success'); | |
| settingsContent.classList.remove('open'); | |
| toggleSettingsBtn.innerHTML = '<i class="fas fa-sliders-h"></i> <span>Timer Settings</span>'; | |
| } | |
| function updateSoundSources() { | |
| timerEndSound.src = SOUND_URLS[settings.timerSound] || ''; | |
| timerEndSound.load(); | |
| breakStartSound.load(); | |
| workStartSound.load(); | |
| // Preload task complete sound | |
| taskCompleteSound.src = 'https://assets.mixkit.co/sfx/preview/mixkit-achievement-bell-600.mp3'; | |
| taskCompleteSound.load(); | |
| // Preload focus alarm sound | |
| focusAlarmSound.src = 'https://assets.mixkit.co/sfx/preview/mixkit-alarm-clock-beep-1103.mp3'; | |
| focusAlarmSound.load(); | |
| } | |
| // Unlock audio on iOS/devices that require user interaction | |
| function unlockAudio() { | |
| const playSilentAudio = () => { | |
| const silentAudio = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...'); | |
| silentAudio.volume = 0; | |
| silentAudio.play().catch(e => console.log('Audio unlock attempt failed')); | |
| }; | |
| document.addEventListener('click', function unlockOnce() { | |
| playSilentAudio(); | |
| document.removeEventListener('click', unlockOnce); | |
| }, { once: true }); | |
| } | |
| // --- Timer Logic --- | |
| function startTimer() { | |
| if (isRunning) return; | |
| // Sound preload attempt (helps with iOS/Android restrictions) | |
| try { | |
| timerEndSound.load(); | |
| breakStartSound.load(); | |
| workStartSound.load(); | |
| const playPromise = timerEndSound.play(); | |
| if (playPromise !== undefined) { | |
| playPromise.then(_ => { | |
| timerEndSound.pause(); | |
| timerEndSound.currentTime = 0; | |
| }).catch(e => {}); | |
| } | |
| } catch (e) {} | |
| isRunning = true; | |
| // Update total seconds based on current mode and settings | |
| switch (currentMode) { | |
| case 'work': totalSeconds = settings.workDuration * 60; break; | |
| case 'break': totalSeconds = settings.breakDuration * 60; break; | |
| case 'longBreak': totalSeconds = settings.longBreakDuration * 60; break; | |
| } | |
| // Cap timeLeft if it exceeds new totalSeconds | |
| if (timeLeft > totalSeconds) timeLeft = totalSeconds; | |
| // Play appropriate sound | |
| if (currentMode === 'work') playSound(workStartSound); | |
| else playSound(breakStartSound); | |
| timerInterval = setInterval(updateTimer, 1000); | |
| updateButtonStates(); | |
| // Show running indicator | |
| soundWave.classList.remove('hidden'); | |
| statusText.textContent = `${currentMode === 'work' ? 'Focusing' : 'On break'}...`; | |
| activityStatus.classList.add('text-purple-600'); | |
| // Show desktop notification if available | |
| if (Notification.permission === 'granted' && document.hidden) { | |
| new Notification(`ZenPomo Pro - ${currentMode === 'work' ? 'Work' : 'Break'} Started`, { | |
| body: `Timer started for ${formatMinutesSeconds(totalSeconds)}.` | |
| }); | |
| } | |
| } | |
| function pauseTimer() { | |
| if (!isRunning) return; | |
| clearInterval(timerInterval); | |
| isRunning = false; | |
| updateButtonStates(); | |
| // Hide running indicator | |
| soundWave.classList.add('hidden'); | |
| statusText.textContent = 'Timer paused'; | |
| activityStatus.classList.remove('text-purple-600'); | |
| } | |
| function resetTimer(preventModeSwitch = false) { | |
| clearInterval(timerInterval); | |
| isRunning = false; | |
| if (!preventModeSwitch) { | |
| currentMode = 'work'; | |
| pomodoroCount = 0; | |
| updateModeIndicator(); | |
| } | |
| // Update time based on current mode and latest settings | |
| switch (currentMode) { | |
| case 'work': timeLeft = settings.workDuration * 60; break; | |
| case 'break': timeLeft = settings.breakDuration * 60; break; | |
| case 'longBreak': timeLeft = settings.longBreakDuration * 60; break; | |
| default: timeLeft = settings.workDuration * 60; | |
| } | |
| totalSeconds = timeLeft; | |
| updateTimerDisplay(); | |
| updateProgressBar(true); | |
| updateButtonStates(); | |
| // Hide running indicator | |
| soundWave.classList.add('hidden'); | |
| statusText.textContent = 'Timer idle'; | |
| activityStatus.classList.remove('text-purple-600'); | |
| } | |
| function updateTimer() { | |
| if (timeLeft > 0) { | |
| timeLeft--; | |
| updateTimerDisplay(); | |
| updateProgressBar(); | |
| // Alert when 1 minute remains | |
| if (timeLeft === 60) { | |
| playSound(focusAlarmSound); | |
| } | |
| } else { | |
| // Timer reached 0 | |
| clearInterval(timerInterval); | |
| isRunning = false; | |
| playSound(timerEndSound); | |
| // Determine next mode | |
| let nextMode = 'work'; | |
| if (currentMode === 'work') { | |
| pomodoroCount++; | |
| recordPomodoroCompletion(); | |
| updateStatsDisplay(); | |
| if (pomodoroCount >= settings.pomodorosBeforeLongBreak) { | |
| nextMode = 'longBreak'; | |
| pomodoroCount = 0; | |
| } else { | |
| nextMode = 'break'; | |
| } | |
| } else { | |
| nextMode = 'work'; | |
| } | |
| // Switch mode | |
| switchMode(nextMode); | |
| // Auto-start if enabled | |
| if (settings.autoStart) { | |
| setTimeout(startTimer, 500); | |
| } else { | |
| updateButtonStates(); | |
| } | |
| } | |
| } | |
| function skipToBreak() { | |
| if (currentMode !== 'work') return; | |
| clearInterval(timerInterval); | |
| isRunning = false; | |
| // Determine break type based on next pomodoro | |
| let nextBreakMode = pomodoroCount + 1 >= settings.pomodorosBeforeLongBreak ? 'longBreak' : 'break'; | |
| showNotification('Work Skipped', `Starting ${nextBreakMode === 'longBreak' ? 'long break' : 'short break'}.`, 'info'); | |
| switchMode(nextBreakMode); | |
| if (settings.autoStart) { | |
| setTimeout(startTimer, 500); | |
| } else { | |
| updateButtonStates(); | |
| } | |
| } | |
| function switchMode(newMode) { | |
| currentMode = newMode; | |
| // Set time left based on new mode and current settings | |
| switch (newMode) { | |
| case 'work': timeLeft = settings.workDuration * 60; break; | |
| case 'break': timeLeft = settings.breakDuration * 60; break; | |
| case 'longBreak': timeLeft = settings.longBreakDuration * 60; break; | |
| default: timeLeft = settings.workDuration * 60; | |
| } | |
| totalSeconds = timeLeft; | |
| updateModeIndicator(); | |
| updateTimerDisplay(); | |
| updateProgressBar(true); | |
| updateButtonStates(); | |
| // Update status text | |
| if (!isRunning) { | |
| statusText.textContent = 'Timer idle'; | |
| } else { | |
| statusText.textContent = `${currentMode === 'work' ? 'Focusing' : 'On break'}...`; | |
| } | |
| // Request notification permission if needed | |
| if (Notification.permission !== 'granted' && Notification.permission !== 'denied') { | |
| Notification.requestPermission().then(permission => { | |
| console.log('Notification permission:', permission); | |
| }); | |
| } | |
| // Send desktop notification if enabled | |
| if (Notification.permission === 'granted' && document.hidden) { | |
| new Notification(`ZenPomo Pro - ${newMode === 'work' ? 'Work' : 'Break'} Time!`, { | |
| body: `It's time to ${newMode === 'work' ? 'focus' : 'take a break'}!`, | |
| }); | |
| } | |
| } | |
| function updateModeIndicator() { | |
| let modeClass = 'px-5 py-3 rounded-full font-semibold flex items-center gap-2 transition-colors'; | |
| let progressColor = 'bg-purple-600'; | |
| let icon, modeName; | |
| switch (currentMode) { | |
| case 'work': | |
| icon = 'fa-bolt'; | |
| modeName = 'Focus'; | |
| modeClass += ' bg-purple-100 text-purple-600'; | |
| break; | |
| case 'break': | |
| icon = 'fa-coffee'; | |
| modeName = 'Break'; | |
| modeClass += ' bg-pink-100 text-pink-500'; | |
| progressColor = 'bg-pink-500'; | |
| break; | |
| case 'longBreak': | |
| icon = 'fa-umbrella-beach'; | |
| modeName = 'Long Break'; | |
| modeClass += ' bg-green-100 text-green-500'; | |
| progressColor = 'bg-green-500'; | |
| break; | |
| } | |
| modeText.innerHTML = `<i class="fas ${icon}"></i> <span>${modeName}</span>`; | |
| modeText.className = modeClass; | |
| progressBar.className = progressColor; | |
| } | |
| function updateButtonStates() { | |
| if (isRunning) { | |
| startBtn.disabled = true; | |
| startBtn.innerHTML = '<i class="fas fa-play"></i> <span class="hidden sm:inline">Running...</span>'; | |
| pauseBtn.disabled = false; | |
| resetBtn.disabled = false; | |
| skipToBreakBtn.disabled = currentMode !== 'work'; | |
| skipToBreakBtn.classList.toggle('hidden', currentMode !== 'work'); | |
| } else { | |
| const isAtStart = timeLeft >= getCurrentPhaseTotalSeconds(); | |
| startBtn.disabled = false; | |
| startBtn.innerHTML = isAtStart ? | |
| '<i class="fas fa-play"></i> <span class="hidden sm:inline">Start</span>' : | |
| '<i class="fas fa-play"></i> <span class="hidden sm:inline">Resume</span>'; | |
| pauseBtn.disabled = true; | |
| resetBtn.disabled = isAtStart; | |
| skipToBreakBtn.disabled = true; | |
| skipToBreakBtn.classList.add('hidden'); | |
| } | |
| } | |
| function getCurrentPhaseTotalSeconds() { | |
| switch (currentMode) { | |
| case 'work': return settings.workDuration * 60; | |
| case 'break': return settings.breakDuration * 60; | |
| case 'longBreak': return settings.longBreakDuration * 60; | |
| default: return 25 * 60; | |
| } | |
| } | |
| function updateTimerDisplay() { | |
| // Format time | |
| const minutes = Math.floor(timeLeft / 60); | |
| const seconds = timeLeft % 60; | |
| const displayTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| timerDisplay.textContent = displayTime; | |
| // Update document title | |
| const modeTitle = currentMode === 'work' ? 'Work' : currentMode === 'break' ? 'Short Break' : 'Long Break'; | |
| document.title = `(${displayTime}) ${modeTitle} - ZenPomo Pro`; | |
| // Change color based on time and mode | |
| if (timeLeft <= 60 && timeLeft > 0) { | |
| timerDisplay.className = 'text-red-500 text-7xl md:text-8xl font-bold text-center my-8 font-mono transition-colors'; | |
| } else { | |
| let colorClass; | |
| switch (currentMode) { | |
| case 'work': colorClass = 'text-purple-600'; break; | |
| case 'break': colorClass = 'text-pink-500'; break; | |
| case 'longBreak': colorClass = 'text-green-500'; break; | |
| } | |
| timerDisplay.className = `${colorClass} text-7xl md:text-8xl font-bold text-center my-8 font-mono transition-colors`; | |
| } | |
| } | |
| function updateProgressBar(reset = false) { | |
| if (reset || totalSeconds <= 0 || timeLeft > totalSeconds) { | |
| progressBar.style.width = '0%'; | |
| return; | |
| } | |
| const currentTimeLeft = Math.max(0, Math.min(timeLeft, totalSeconds)); | |
| const progress = totalSeconds > 0 ? ((totalSeconds - currentTimeLeft) / totalSeconds) * 100 : 0; | |
| progressBar.style.width = `${progress}%`; | |
| } | |
| function formatMinutesSeconds(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins} minute${mins !== 1 ? 's' : ''} ${secs} second${secs !== 1 ? 's' : ''}`; | |
| } | |
| function playSound(soundElement) { | |
| if ((!soundElement.src && soundElement !== taskCompleteSound) || | |
| (settings.timerSound === 'none' && soundElement !== taskCompleteSound)) { | |
| return; | |
| } | |
| try { | |
| soundElement.currentTime = 0; | |
| const playPromise = soundElement.play(); | |
| if (playPromise !== undefined) { | |
| playPromise.then(_ => { | |
| // console.log(`Sound ${soundElement.id} played`); | |
| }).catch(error => { | |
| console.error('Sound play failed:', error); | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Sound error:', error); | |
| } | |
| } | |
| // --- Task Management --- | |
| function loadTasks() { | |
| const saved = localStorage.getItem('zenpomoTasks'); | |
| return saved ? JSON.parse(saved) : []; | |
| } | |
| function saveTasks() { | |
| localStorage.setItem('zenpomoTasks', JSON.stringify(tasks)); | |
| } | |
| function addTask(taskData) { | |
| const newTask = { | |
| id: taskData.id || Date.now(), | |
| text: taskData.text, | |
| category: taskData.category || 'other', | |
| priority: taskData.priority || 'medium', | |
| dueDate: taskData.dueDate || null, | |
| notes: taskData.notes || '', | |
| completed: taskData.completed || false, | |
| createdAt: taskData.createdAt || new Date().toISOString() | |
| }; | |
| tasks.push(newTask); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskStats(); | |
| scrollToTask(newTask.id); | |
| return newTask; | |
| } | |
| function updateTask(taskId, updatedData) { | |
| tasks = tasks.map(task => | |
| task.id === taskId ? { ...task, ...updatedData } : task | |
| ); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskStats(); | |
| scrollToTask(taskId); | |
| } | |
| function deleteTask(taskId) { | |
| tasks = tasks.filter(task => task.id !== taskId); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskStats(); | |
| showNotification('Task Deleted', 'The task has been removed.', 'info'); | |
| } | |
| function toggleTaskComplete(taskId) { | |
| let taskCompleted = false; | |
| tasks = tasks.map(task => { | |
| if (task.id === taskId) { | |
| taskCompleted = !task.completed; | |
| return { ...task, completed: taskCompleted }; | |
| } | |
| return task; | |
| }); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskStats(); | |
| if (taskCompleted) { | |
| playSound(taskCompleteSound); | |
| showNotification('Task Completed!', 'Well done!', 'success'); | |
| } | |
| } | |
| function clearCompletedTasks() { | |
| const completedCount = tasks.filter(t => t.completed).length; | |
| if (completedCount === 0) { | |
| showNotification('No Completed Tasks', 'Nothing to clear.', 'info'); | |
| return; | |
| } | |
| tasks = tasks.filter(task => !task.completed); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskStats(); | |
| showNotification('Tasks Cleared', `${completedCount} completed task(s) removed.`, 'success'); | |
| } | |
| function scrollToTask(taskId) { | |
| const taskElement = document.querySelector(`.task-item[data-id="${taskId}"]`); | |
| if (taskElement) { | |
| taskElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| // Add highlight effect | |
| taskElement.classList.add('bg-yellow-50'); | |
| setTimeout(() => taskElement.classList.remove('bg-yellow-50'), 1000); | |
| } | |
| } | |
| function renderTasks() { | |
| // Filter by current category | |
| const filtered = tasks.filter(t => | |
| currentCategoryFilter === 'all' || t.category === currentCategoryFilter | |
| ); | |
| // Sort: incomplete first, then by priority (high to low), then by creation date (newest first) | |
| filtered.sort((a, b) => | |
| (a.completed - b.completed) || | |
| ({ high: 1, medium: 2, low: 3 }[a.priority] - { high: 1, medium: 2, low: 3 }[b.priority]) || | |
| (new Date(b.createdAt) - new Date(a.createdAt)) | |
| ); | |
| taskList.innerHTML = ''; | |
| if (filtered.length === 0) { | |
| emptyState.classList.remove('hidden'); | |
| taskList.classList.add('hidden'); | |
| } else { | |
| emptyState.classList.add('hidden'); | |
| taskList.classList.remove('hidden'); | |
| filtered.forEach(task => { | |
| const isOverdue = task.dueDate && !task.completed && | |
| new Date(task.dueDate + 'T00:00:00') < new Date().setHours(0,0,0,0); | |
| const formattedDueDate = task.dueDate ? | |
| new Date(task.dueDate + 'T00:00:00').toLocaleDateString(undefined, { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| }) : ''; | |
| const taskItem = document.createElement('li'); | |
| taskItem.className = `task-item flex items-center p-4 bg-gray-50 rounded-xl mb-3 transition-all hover:translate-x-1 ${task.completed ? 'opacity-80 bg-gray-100' : ''}`; | |
| taskItem.dataset.id = task.id; | |
| // Priority indicator | |
| taskItem.innerHTML = ` | |
| <div class="priority-dot w-2 h-2 rounded-full mr-3 ${PRIORITY_COLORS[task.priority]}" title="Priority: ${task.priority}"></div> | |
| <input type="checkbox" class="w-5 h-5 mr-3 cursor-pointer" ${task.completed ? 'checked' : ''}> | |
| <div class="flex-1 overflow-hidden"> | |
| <span class="task-text block mb-1 ${task.completed ? 'line-through text-gray-500' : 'text-gray-800'}">${escapeHTML(task.text)}</span> | |
| <div class="flex flex-wrap items-center gap-2 text-xs"> | |
| ${task.category ? `<span class="task-category px-2 py-0.5 rounded-full bg-purple-100 text-purple-600">${task.category}</span>` : ''} | |
| ${formattedDueDate ? ` | |
| <span class="flex items-center gap-1 ${isOverdue ? 'text-red-500 font-semibold' : 'text-gray-500'}"> | |
| <i class="far fa-calendar-alt"></i> | |
| <span>${formattedDueDate} ${isOverdue ? '(Overdue)' : ''}</span> | |
| </span>` : ''} | |
| ${task.notes ? `<span class="flex items-center gap-1 text-gray-500"><i class="far fa-sticky-note"></i> <span>Has notes</span></span>` : ''} | |
| </div> | |
| </div> | |
| <div class="task-actions flex gap-1 opacity-0 transition-opacity"> | |
| <button class="edit-btn w-7 h-7 rounded-full flex items-center justify-center hover:bg-gray-200 transition-all text-gray-500 hover:text-blue-500" title="Edit Task"> | |
| <i class="far fa-edit"></i> | |
| </button> | |
| <button class="delete-btn w-7 h-7 rounded-full flex items-center justify-center hover:bg-gray-200 transition-all text-gray-500 hover:text-red-500" title="Delete Task"> | |
| <i class="far fa-trash-alt"></i> | |
| </button> | |
| </div> | |
| `; | |
| // Add event listeners | |
| taskItem.querySelector('input[type="checkbox"]').addEventListener('change', () => toggleTaskComplete(task.id)); | |
| taskItem.querySelector('.edit-btn').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| openEditTaskModal(task.id); | |
| }); | |
| taskItem.querySelector('.delete-btn').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| deleteTask(task.id); | |
| }); | |
| // Click anywhere on task to toggle (unless clicking buttons) | |
| taskItem.addEventListener('click', (e) => { | |
| if (!e.target.closest('button') && !e.target.closest('input[type="checkbox"]')) { | |
| toggleTaskComplete(task.id); | |
| } | |
| }); | |
| taskList.appendChild(taskItem); | |
| }); | |
| } | |
| updateTaskStats(); | |
| } | |
| function updateTaskStats() { | |
| const total = tasks.length; | |
| const completed = tasks.filter(t => t.completed).length; | |
| totalTasksSpan.textContent = `${total} ${total === 1 ? 'task' : 'tasks'}`; | |
| completedTasksSpan.textContent = `${completed} completed`; | |
| } | |
| function handleAddQuickTask() { | |
| const text = newTaskInput.value.trim(); | |
| if (!text) { | |
| showNotification('Empty Task', 'Please enter task text.', 'error'); | |
| newTaskInput.focus(); | |
| return; | |
| } | |
| const category = currentCategoryFilter !== 'all' ? currentCategoryFilter : 'other'; | |
| addTask({ text, category }); | |
| newTaskInput.value = ''; | |
| showNotification('Task Added', 'Quick task added successfully!', 'success'); | |
| } | |
| // --- Task Modal Functions --- | |
| function openAddTaskModal() { | |
| currentTaskEditingId = null; | |
| modalTaskTitle.textContent = 'Add New Task'; | |
| taskIdInput.value = ''; | |
| taskNameInput.value = ''; | |
| taskCategoryInput.value = currentCategoryFilter !== 'all' ? currentCategoryFilter : 'work'; | |
| taskPriorityInput.value = 'medium'; | |
| taskDueDateInput.value = ''; | |
| taskNotesInput.value = ''; | |
| taskModal.classList.add('open'); | |
| taskNameInput.focus(); | |
| } | |
| function openEditTaskModal(taskId) { | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task) return; | |
| currentTaskEditingId = taskId; | |
| modalTaskTitle.textContent = 'Edit Task'; | |
| taskIdInput.value = task.id; | |
| taskNameInput.value = task.text; | |
| taskCategoryInput.value = task.category; | |
| taskPriorityInput.value = task.priority; | |
| taskDueDateInput.value = task.dueDate || ''; | |
| taskNotesInput.value = task.notes || ''; | |
| taskModal.classList.add('open'); | |
| taskNameInput.focus(); | |
| } | |
| function closeTaskModal() { | |
| taskModal.classList.remove('open'); | |
| // Reset modal after animation | |
| setTimeout(() => { | |
| currentTaskEditingId = null; | |
| modalTaskTitle.textContent = 'Add New Task'; | |
| taskIdInput.value = ''; | |
| taskNameInput.value = ''; | |
| taskCategoryInput.value = 'work'; | |
| taskPriorityInput.value = 'medium'; | |
| taskDueDateInput.value = ''; | |
| taskNotesInput.value = ''; | |
| }, 300); | |
| } | |
| function saveTaskFromModal() { | |
| const text = taskNameInput.value.trim(); | |
| if (!text) { | |
| showNotification('Error', 'Task name cannot be empty!', 'error'); | |
| taskNameInput.focus(); | |
| return; | |
| } | |
| const taskData = { | |
| text, | |
| category: taskCategoryInput.value, | |
| priority: taskPriorityInput.value, | |
| dueDate: taskDueDateInput.value || null, | |
| notes: taskNotesInput.value.trim() | |
| }; | |
| if (currentTaskEditingId) { | |
| updateTask(currentTaskEditingId, taskData); | |
| showNotification('Task Updated', 'Task details saved.', 'success'); | |
| } else { | |
| addTask(taskData); | |
| showNotification('Task Added', 'New task created.', 'success'); | |
| } | |
| closeTaskModal(); | |
| } | |
| // --- Statistics Management --- | |
| function defaultStats() { | |
| return { | |
| pomodoros: {}, | |
| totalPomodoros: 0, | |
| streak: 0, | |
| lastActivityDate: null, | |
| lastResetDate: new Date().toISOString() | |
| }; | |
| } | |
| function loadStats() { | |
| const saved = localStorage.getItem('zenpomoStats'); | |
| let loaded = saved ? JSON.parse(saved) : defaultStats(); | |
| loaded = { ...defaultStats(), ...loaded }; | |
| // Ensure required properties exist | |
| if (!loaded.pomodoros) loaded.pomodoros = {}; | |
| if (!loaded.totalPomodoros) loaded.totalPomodoros = 0; | |
| if (!loaded.lastActivityDate) loaded.lastActivityDate = null; | |
| // Update streak if needed | |
| updateStreak(loaded); | |
| return loaded; | |
| } | |
| function updateStreak(statsObj) { | |
| if (!statsObj.lastActivityDate) { | |
| statsObj.streak = 0; | |
| return; | |
| } | |
| const lastDate = new Date(statsObj.lastActivityDate); | |
| const today = new Date(); | |
| const yesterday = new Date(today); | |
| yesterday.setDate(yesterday.getDate() - 1); | |
| // Reset streak if last activity was before yesterday | |
| if (lastDate.toDateString() !== today.toDateString() && | |
| lastDate.toDateString() !== yesterday.toDateString()) { | |
| statsObj.streak = 0; | |
| } | |
| } | |
| function saveStats() { | |
| localStorage.setItem('zenpomoStats', JSON.stringify(stats)); | |
| } | |
| function recordPomodoroCompletion() { | |
| const today = new Date(); | |
| const dateStr = today.toISOString().split('T')[0]; | |
| // Update count for today | |
| stats.pomodoros[dateStr] = (stats.pomodoros[dateStr] || 0) + 1; | |
| stats.totalPomodoros = (stats.totalPomodoros || 0) + 1; | |
| // Update streak | |
| const yesterday = new Date(today); | |
| yesterday.setDate(yesterday.getDate() - 1); | |
| const yesterdayStr = yesterday.toISOString().split('T')[0]; | |
| if (!stats.lastActivityDate || | |
| stats.lastActivityDate === yesterdayStr || | |
| stats.lastActivityDate === dateStr) { | |
| // Continue streak | |
| stats.lastActivityDate = dateStr; | |
| stats.streak = (stats.streak || 0) + 1; | |
| } else { | |
| // Reset streak | |
| stats.lastActivityDate = dateStr; | |
| stats.streak = 1; | |
| } | |
| saveStats(); | |
| } | |
| function getPomodorosForDate(dateStr) { | |
| return stats.pomodoros[dateStr] || 0; | |
| } | |
| function getPomodorosThisWeek() { | |
| const today = new Date(); | |
| const startOfWeek = new Date(today); | |
| startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay()); // Sunday | |
| let count = 0; | |
| for (let i = 0; i < 7; i++) { | |
| const date = new Date(startOfWeek); | |
| date.setDate(date.getDate() + i); | |
| const dateStr = date.toISOString().split('T')[0]; | |
| count += getPomodorosForDate(dateStr); | |
| } | |
| return count; | |
| } | |
| function getTotalFocusTime() { | |
| return (stats.totalPomodoros || 0) * settings.workDuration; | |
| } | |
| function updateStatsDisplay() { | |
| const todayStr = new Date().toISOString().split('T')[0]; | |
| const todayCount = getPomodorosForDate(todayStr); | |
| const weekCount = getPomodorosThisWeek(); | |
| const totalCount = stats.totalPomodoros || 0; | |
| // Format total focus time | |
| const totalMinutes = getTotalFocusTime(); | |
| const hours = Math.floor(totalMinutes / 60); | |
| const minutes = totalMinutes % 60; | |
| // Update main display | |
| todayPomodorosDisplay.textContent = todayCount; | |
| weekPomodorosDisplay.textContent = weekCount; | |
| totalPomodorosDisplay.textContent = totalCount; | |
| // Update modal display | |
| modalTodayPomodoros.textContent = todayCount; | |
| modalWeekPomodoros.textContent = weekCount; | |
| modalTotalPomodoros.textContent = totalCount; | |
| modalTotalFocusTime.textContent = `${hours}h ${minutes}m`; | |
| modalCurrentStreak.textContent = `${stats.streak || 0} ${stats.streak === 1 ? 'day' : 'days'}`; | |
| renderHistoryList(); | |
| } | |
| function renderHistoryList() { | |
| const sortedDates = Object.keys(stats.pomodoros) | |
| .sort((a, b) => new Date(b) - new Date(a)) | |
| .slice(0, 20); // Show only recent 20 items | |
| historyList.innerHTML = ''; | |
| if (sortedDates.length === 0) { | |
| historyEmptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| historyEmptyState.classList.add('hidden'); | |
| sortedDates.forEach(dateStr => { | |
| const count = stats.pomodoros[dateStr]; | |
| const date = new Date(dateStr + 'T00:00:00'); | |
| const formattedDate = date.toLocaleDateString(undefined, { | |
| weekday: 'short', | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| }); | |
| const item = document.createElement('div'); | |
| item.className = 'history-item flex justify-between items-center px-4 py-3 border-b border-gray-200 last:border-b-0'; | |
| item.innerHTML = ` | |
| <span class="history-date font-medium">${formattedDate}</span> | |
| <span class="history-count px-2 py-0.5 rounded-full bg-purple-100 text-purple-600 font-semibold"> | |
| ${count} ${count === 1 ? 'session' : 'sessions'} | |
| </span> | |
| `; | |
| historyList.appendChild(item); | |
| }); | |
| } | |
| function resetAllStats() { | |
| if (confirm("Are you sure you want to reset ALL statistics? This cannot be undone.")) { | |
| stats = defaultStats(); | |
| stats.lastResetDate = new Date().toISOString(); | |
| saveStats(); | |
| updateStatsDisplay(); | |
| showNotification('Stats Reset', 'All statistics have been cleared.', 'success'); | |
| } | |
| } | |
| // --- Stats Modal Functions --- | |
| function openStatsModal() { | |
| updateStatsDisplay(); | |
| statsModal.classList.add('open'); | |
| activateStatsTab('history'); | |
| } | |
| function closeStatsModal() { | |
| statsModal.classList.remove('open'); | |
| } | |
| function activateStatsTab(tabId) { | |
| // Update tabs | |
| statsTabs.forEach(tab => { | |
| tab.classList.toggle('text-purple-600', tab.dataset.tab === tabId); | |
| tab.classList.toggle('border-b-2', tab.dataset.tab === tabId); | |
| tab.classList.toggle('border-purple-600', tab.dataset.tab === tabId); | |
| }); | |
| // Update content | |
| statsTabContents.forEach(content => { | |
| content.classList.toggle('hidden', content.id !== `${tabId}-tab`); | |
| }); | |
| } | |
| // --- Notifications --- | |
| function showNotification(title, message, type = 'info') { | |
| clearTimeout(notificationTimeout); | |
| // Set notification content | |
| notificationTitle.textContent = title; | |
| notificationMessage.textContent = message; | |
| // Set notification style | |
| notification.className = 'fixed bottom-6 right-6 bg-white rounded-lg shadow-lg p-4 flex items-center gap-3 max-w-sm w-full border-l-4'; | |
| switch (type) { | |
| case 'success': | |
| notificationIcon.className = 'fas fa-check-circle text-green-500 text-xl'; | |
| notification.classList.add('border-green-500'); | |
| break; | |
| case 'error': | |
| notificationIcon.className = 'fas fa-times-circle text-red-500 text-xl'; | |
| notification.classList.add('border-red-500'); | |
| break; | |
| case 'info': | |
| notificationIcon.className = 'fas fa-info-circle text-blue-500 text-xl'; | |
| notification.classList.add('border-blue-500'); | |
| break; | |
| } | |
| // Show notification | |
| notification.classList.remove('hidden', 'opacity-0'); | |
| notification.classList.add('show'); | |
| // Auto-hide after 5 seconds | |
| notificationTimeout = setTimeout(() => { | |
| notification.classList.remove('show'); | |
| }, 5000); | |
| } | |
| function hideNotification() { | |
| clearTimeout(notificationTimeout); | |
| notification.classList.remove('show'); | |
| } | |
| // --- Utility Functions --- | |
| function escapeHTML(str) { | |
| const div = document.createElement('div'); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| // --- Fullscreen Mode --- | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen().catch(err => { | |
| console.error('Fullscreen error:', err); | |
| showNotification('Fullscreen Error', 'Could not enter fullscreen mode.', 'error'); | |
| }); | |
| isFullscreen = true; | |
| } else { | |
| if (document.exitFullscreen) { | |
| document.exitFullscreen(); | |
| isFullscreen = false; | |
| } | |
| } | |
| } | |
| // --- Event Listeners --- | |
| // Timer controls | |
| startBtn.addEventListener('click', startTimer); | |
| pauseBtn.addEventListener('click', pauseTimer); | |
| resetBtn.addEventListener('click', () => resetTimer(false)); | |
| skipToBreakBtn.addEventListener('click', skipToBreak); | |
| fullscreenBtn.addEventListener('click', toggleFullscreen); | |
| // Settings | |
| applySettingsBtn.addEventListener('click', applySettings); | |
| toggleSettingsBtn.addEventListener('click', function() { | |
| settingsContent.classList.toggle('open'); | |
| this.innerHTML = settingsContent.classList.contains('open') ? | |
| '<i class="fas fa-chevron-up"></i> <span>Hide Settings</span>' : | |
| '<i class="fas fa-sliders-h"></i> <span>Timer Settings</span>'; | |
| }); | |
| // Tasks | |
| addTaskBtn.addEventListener('click', handleAddQuickTask); | |
| newTaskInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') handleAddQuickTask(); | |
| }); | |
| clearCompletedBtn.addEventListener('click', clearCompletedTasks); | |
| categoryBtns.forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| categoryBtns.forEach(b => { | |
| b.classList.remove('bg-purple-600', 'text-white'); | |
| b.classList.add('bg-gray-200', 'hover:bg-gray-300'); | |
| }); | |
| this.classList.remove('bg-gray-200', 'hover:bg-gray-300'); | |
| this.classList.add('bg-purple-600', 'text-white'); | |
| currentCategoryFilter = this.dataset.category; | |
| renderTasks(); | |
| }); | |
| }); | |
| // Task modal | |
| addTaskModalBtn.addEventListener('click', openAddTaskModal); | |
| closeModalBtn.addEventListener('click', closeTaskModal); | |
| cancelTaskBtn.addEventListener('click', closeTaskModal); | |
| saveTaskBtn.addEventListener('click', saveTaskFromModal); | |
| taskModal.addEventListener('click', (e) => { | |
| if (e.target === taskModal) closeTaskModal(); | |
| }); | |
| // Stats modal | |
| statsBtn.addEventListener('click', openStatsModal); | |
| closeStatsModalBtn.addEventListener('click', closeStatsModal); | |
| statsModal.addEventListener('click', (e) => { | |
| if (e.target === statsModal) closeStatsModal(); | |
| }); | |
| resetStatsBtn.addEventListener('click', resetAllStats); | |
| // Tabs in stats modal | |
| statsTabs.forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| activateStatsTab(this.dataset.tab); | |
| }); | |
| }); | |
| // Notification | |
| closeNotificationBtn.addEventListener('click', hideNotification); | |
| // Navigation links | |
| document.getElementById('nav-stats')?.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| statsBtn.click(); | |
| }); | |
| document.getElementById('nav-tasks')?.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| document.querySelector('.todo-card').scrollIntoView({ behavior: 'smooth' }); | |
| }); | |
| document.getElementById('nav-settings')?.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| document.querySelector('.pomodoro-card').scrollIntoView({ behavior: 'smooth' }); | |
| if (!settingsContent.classList.contains('open')) { | |
| toggleSettingsBtn.click(); | |
| } | |
| }); | |
| // Fullscreen change listener | |
| document.addEventListener('fullscreenchange', () => { | |
| isFullscreen = !!document.fullscreenElement; | |
| fullscreenBtn.innerHTML = isFullscreen ? | |
| '<i class="fas fa-compress"></i>' : | |
| '<i class="fas fa-expand"></i>'; | |
| fullscreenBtn.title = isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'; | |
| }); | |
| // Page visibility change (for notifications) | |
| document.addEventListener('visibilitychange', () => { | |
| if (!document.hidden && isRunning) { | |
| const modeName = currentMode === 'work' ? 'Work' : currentMode === 'break' ? 'Short Break' : 'Long Break'; | |
| showNotification('ZenPomo Pro Reminder', `${modeName} timer is running: ${timerDisplay.textContent} remaining`, 'info'); | |
| } | |
| }); | |
| // Initialize the app | |
| initializeApp(); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=azizln/zenpomo" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |