Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Simple 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> | |
| #timer-widget { | |
| user-select: none; | |
| transition: all 0.3s ease; | |
| width: 180px ; | |
| height: 80px ; | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| background: rgba(0, 0, 0, 0.3); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| } | |
| #timer-widget.expanded { | |
| width: 300px ; | |
| height: 200px ; | |
| } | |
| #controls-container, | |
| #note-container { | |
| display: none; | |
| } | |
| #timer-widget.expanded #controls-container, | |
| #timer-widget.expanded #note-container { | |
| display: flex; | |
| } | |
| #note-text { | |
| display: none; | |
| font-size: 0.8rem; | |
| margin-top: 4px; | |
| color: rgba(255, 255, 255, 0.7); | |
| word-break: break-word; | |
| } | |
| #timer-widget.has-note #note-text { | |
| display: block; | |
| } | |
| #live-clock { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| color: rgba(255, 255, 255, 0.9); | |
| font-weight: 300; | |
| letter-spacing: 1px; | |
| font-size: 1.8rem; | |
| cursor: pointer; | |
| } | |
| #live-date { | |
| color: rgba(255, 255, 255, 0.6); | |
| font-weight: 300; | |
| } | |
| #time-note { | |
| transition: all 0.2s ease; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| #time-note:focus { | |
| outline: none; | |
| background-color: rgba(255, 255, 255, 0.08) ; | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| #timer-widget { | |
| overflow: hidden; | |
| min-width: 180px; | |
| min-height: 80px; | |
| border-radius: 16px; | |
| } | |
| .timer-button { | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.15); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| transition: all 0.2s ease; | |
| } | |
| .timer-button:hover { | |
| background: rgba(255, 255, 255, 0.15); | |
| border-color: rgba(255, 255, 255, 0.25); | |
| } | |
| .timer-button:active { | |
| transform: scale(0.95); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900"> | |
| <!-- Draggable Time Widget --> | |
| <div id="timer-widget" class="fixed bottom-4 right-4 z-50 cursor-move"> | |
| <div class="rounded-xl p-4 text-white h-full flex items-center justify-center"> | |
| <div class="flex flex-col items-center" id="timer-content"> | |
| <div id="live-clock">00:00:00</div> | |
| <div class="text-sm opacity-80 mt-1" id="live-date">Loading...</div> | |
| <div id="note-text" class="hovered-element">zxcvbnmfjfgiygwyigfiweayhsssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssiyglq</div> | |
| <div class="flex space-x-3 mt-4" id="controls-container"> | |
| <button id="start-btn" class="timer-button px-3 py-2 rounded-full text-green-400 hover:text-green-300"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <button id="stop-btn" class="timer-button px-3 py-2 rounded-full text-red-400 hover:text-red-300"> | |
| <i class="fas fa-stop"></i> | |
| </button> | |
| <button id="reset-btn" class="timer-button px-3 py-2 rounded-full text-blue-400 hover:text-blue-300"> | |
| <i class="fas fa-undo"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-4" id="note-container"> | |
| <input type="text" id="time-note" class="text-white text-sm rounded-lg px-3 py-2 w-full" | |
| placeholder="Add note..."> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let timerInterval; | |
| let isRunning = false; | |
| let startTime; | |
| let elapsedTime = 0; | |
| let isExpanded = false; | |
| const widget = document.getElementById('timer-widget'); | |
| const liveClock = document.getElementById('live-clock'); | |
| // Timer functions | |
| function startTimer() { | |
| if (!isRunning) { | |
| startTime = Date.now() - elapsedTime; | |
| timerInterval = setInterval(updateTimer, 10); // More frequent updates for smoother animation | |
| isRunning = true; | |
| updateTimerDisplay(); // Update display mode | |
| } | |
| } | |
| function stopTimer() { | |
| if (isRunning) { | |
| clearInterval(timerInterval); | |
| isRunning = false; | |
| updateTimerDisplay(); // Update display mode | |
| } | |
| } | |
| function resetTimer() { | |
| stopTimer(); | |
| elapsedTime = 0; | |
| updateDisplay(0); | |
| // Save reset state | |
| localStorage.setItem('timerElapsed', 0); | |
| localStorage.setItem('timerRunning', false); | |
| updateTimerDisplay(); // Update display mode | |
| } | |
| function updateTimer() { | |
| elapsedTime = Date.now() - startTime; | |
| updateDisplay(elapsedTime); | |
| } | |
| function updateDisplay(time) { | |
| elapsedTime = time; | |
| document.getElementById('live-clock').textContent = formatTime(time); | |
| } | |
| // Drag functionality | |
| let isDragging = false; | |
| let offsetX, offsetY; | |
| widget.addEventListener('mousedown', (e) => { | |
| if (e.target === widget || e.target.closest('.cursor-move')) { | |
| isDragging = true; | |
| offsetX = e.clientX - widget.getBoundingClientRect().left; | |
| offsetY = e.clientY - widget.getBoundingClientRect().top; | |
| widget.style.transition = 'none'; | |
| e.preventDefault(); | |
| } | |
| }); | |
| document.addEventListener('mousemove', (e) => { | |
| if (!isDragging) return; | |
| widget.style.left = `${e.clientX - offsetX}px`; | |
| widget.style.top = `${e.clientY - offsetY}px`; | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| widget.style.transition = 'all 0.2s ease'; | |
| // Save position to localStorage | |
| const rect = widget.getBoundingClientRect(); | |
| localStorage.setItem('timerPosition', JSON.stringify({ | |
| left: rect.left, | |
| top: rect.top | |
| })); | |
| }); | |
| // Load saved position | |
| const savedPosition = localStorage.getItem('timerPosition'); | |
| if (savedPosition) { | |
| const { left, top } = JSON.parse(savedPosition); | |
| widget.style.left = `${left}px`; | |
| widget.style.top = `${top}px`; | |
| } else { | |
| // Set default position if no saved position | |
| widget.style.left = 'calc(100vw - 200px)'; | |
| widget.style.top = 'calc(100vh - 100px)'; | |
| } | |
| // Toggle expanded state | |
| liveClock.addEventListener('click', (e) => { | |
| e.stopPropagation(); // Prevent triggering drag | |
| isExpanded = !isExpanded; | |
| widget.classList.toggle('expanded', isExpanded); | |
| localStorage.setItem('timerExpanded', isExpanded); | |
| }); | |
| // Button event listeners | |
| document.getElementById('start-btn').addEventListener('click', startTimer); | |
| document.getElementById('stop-btn').addEventListener('click', stopTimer); | |
| document.getElementById('reset-btn').addEventListener('click', resetTimer); | |
| // Load expanded state | |
| const savedExpanded = localStorage.getItem('timerExpanded') === 'true'; | |
| if (savedExpanded) { | |
| isExpanded = true; | |
| widget.classList.add('expanded'); | |
| } | |
| // Update timer display based on state | |
| function updateTimerDisplay() { | |
| // Always show timer when timer is active (running or has elapsed time) | |
| if (isRunning || elapsedTime > 0) { | |
| document.getElementById('live-clock').textContent = formatTime(elapsedTime); | |
| document.getElementById('live-date').style.display = 'none'; | |
| } else { | |
| // Show timer with 00:00:00 when not active | |
| document.getElementById('live-clock').textContent = '00:00:00'; | |
| document.getElementById('live-date').style.display = 'none'; | |
| } | |
| } | |
| // Format time function | |
| function formatTime(time) { | |
| const totalSeconds = Math.floor(time / 1000); | |
| const seconds = totalSeconds % 60; | |
| const minutes = Math.floor((totalSeconds / 60) % 60); | |
| const hours = Math.floor(totalSeconds / 3600); | |
| return [ | |
| hours.toString().padStart(2, '0'), | |
| minutes.toString().padStart(2, '0'), | |
| seconds.toString().padStart(2, '0') | |
| ].join(':'); | |
| } | |
| // Remove live clock functionality entirely | |
| // Update timer display immediately and every second | |
| updateTimerDisplay(); | |
| setInterval(updateTimerDisplay, 1000); | |
| // Save notes to localStorage | |
| const noteInput = document.getElementById('time-note'); | |
| noteInput.addEventListener('input', (e) => { | |
| e.stopPropagation(); // Prevent triggering drag | |
| const note = noteInput.value; | |
| localStorage.setItem('timeNote', note); | |
| if (note.trim()) { | |
| document.getElementById('note-text').textContent = note; | |
| widget.classList.add('has-note'); | |
| } else { | |
| widget.classList.remove('has-note'); | |
| } | |
| }); | |
| // Prevent note input from triggering drag | |
| noteInput.addEventListener('mousedown', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| noteInput.addEventListener('touchstart', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| // Load and show note if exists | |
| const savedNote = localStorage.getItem('timeNote'); | |
| if (savedNote) { | |
| noteInput.value = savedNote; | |
| document.getElementById('note-text').textContent = savedNote; | |
| widget.classList.add('has-note'); | |
| } | |
| // Load timer state from localStorage | |
| const savedElapsed = localStorage.getItem('timerElapsed'); | |
| const savedRunning = localStorage.getItem('timerRunning') === 'true'; | |
| if (savedElapsed) { | |
| elapsedTime = parseInt(savedElapsed); | |
| updateDisplay(elapsedTime); | |
| if (savedRunning) { | |
| startTimer(); | |
| } else { | |
| updateTimerDisplay(); // Update display mode for stopped timer | |
| } | |
| } else { | |
| // Initialize with stopped state | |
| resetTimer(); | |
| } | |
| // Save timer state to localStorage when running | |
| function saveTimerState() { | |
| localStorage.setItem('timerElapsed', elapsedTime); | |
| localStorage.setItem('timerRunning', isRunning); | |
| } | |
| // Save state every second when running | |
| setInterval(() => { | |
| if (isRunning) { | |
| saveTimerState(); | |
| } | |
| }, 1000); | |
| // Make sure widget stays visible when minimized | |
| widget.style.minWidth = '150px'; | |
| widget.style.minHeight = '70px'; | |
| // Ensure widget is always draggable and visible | |
| widget.style.position = 'fixed'; | |
| widget.style.zIndex = '9999'; | |
| // Prevent text selection while dragging | |
| document.addEventListener('selectstart', (e) => { | |
| if (isDragging) { | |
| e.preventDefault(); | |
| } | |
| }); | |
| // Touch support for mobile dragging | |
| widget.addEventListener('touchstart', (e) => { | |
| // Don't start drag if clicking on note input or buttons | |
| if (e.target === noteInput || e.target.closest('button') || e.target.closest('#controls-container') || e.target.closest('#note-container')) { | |
| return; | |
| } | |
| if (e.target === widget || e.target.closest('.cursor-move')) { | |
| isDragging = true; | |
| const touch = e.touches[0]; | |
| offsetX = touch.clientX - widget.getBoundingClientRect().left; | |
| offsetY = touch.clientY - widget.getBoundingClientRect().top; | |
| widget.style.transition = 'none'; | |
| e.preventDefault(); | |
| } | |
| }); | |
| document.addEventListener('touchmove', (e) => { | |
| if (!isDragging) return; | |
| const touch = e.touches[0]; | |
| widget.style.left = `${touch.clientX - offsetX}px`; | |
| widget.style.top = `${touch.clientY - offsetY}px`; | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('touchend', () => { | |
| if (isDragging) { | |
| isDragging = false; | |
| widget.style.transition = 'all 0.2s ease'; | |
| // Save position to localStorage | |
| const rect = widget.getBoundingClientRect(); | |
| localStorage.setItem('timerPosition', JSON.stringify({ | |
| left: rect.left, | |
| top: rect.top | |
| })); | |
| } | |
| }); | |
| // Prevent button clicks from triggering drag | |
| document.querySelectorAll('button').forEach(button => { | |
| button.addEventListener('mousedown', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| button.addEventListener('touchstart', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |