Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dog Potty Tracker</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 { | |
| font-family: 'Courier New', monospace; | |
| } | |
| .event-table { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .delete-btn { | |
| transition: all 0.2s; | |
| } | |
| .delete-btn:hover { | |
| transform: scale(1.1); | |
| color: #ef4444; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-b from-blue-50 to-purple-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold text-blue-600 mb-2">🐶 Dog Potty Tracker</h1> | |
| <p class="text-gray-600">Keep track of your dog's bathroom activities</p> | |
| </header> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> | |
| <!-- Pee Section --> | |
| <div class="bg-white rounded-xl shadow-lg p-6 text-center"> | |
| <div class="flex justify-center items-center mb-4"> | |
| <i class="fas fa-tint text-blue-500 text-4xl mr-3"></i> | |
| <h2 class="text-2xl font-semibold text-blue-700">Pee Tracker</h2> | |
| </div> | |
| <div class="timer text-5xl font-bold text-blue-800 mb-6" id="pee-timer"> | |
| 00:00:00 | |
| </div> | |
| <button id="pee-btn" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full text-lg transition-all transform hover:scale-105"> | |
| <i class="fas fa-plus mr-2"></i> Record Pee | |
| </button> | |
| <div id="pee-confirmation" class="mt-4 text-green-600 font-medium hidden fade-in"> | |
| <i class="fas fa-check-circle mr-2"></i> Pee recorded! | |
| </div> | |
| </div> | |
| <!-- Poop Section --> | |
| <div class="bg-white rounded-xl shadow-lg p-6 text-center"> | |
| <div class="flex justify-center items-center mb-4"> | |
| <i class="fas fa-poop text-amber-700 text-4xl mr-3"></i> | |
| <h2 class="text-2xl font-semibold text-amber-800">Poop Tracker</h2> | |
| </div> | |
| <div class="timer text-5xl font-bold text-amber-800 mb-6" id="poop-timer"> | |
| 00:00:00 | |
| </div> | |
| <button id="poop-btn" class="bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 px-6 rounded-full text-lg transition-all transform hover:scale-105"> | |
| <i class="fas fa-plus mr-2"></i> Record Poop | |
| </button> | |
| <div id="poop-confirmation" class="mt-4 text-green-600 font-medium hidden fade-in"> | |
| <i class="fas fa-check-circle mr-2"></i> Poop recorded! | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Event History --> | |
| <div class="bg-white rounded-xl shadow-lg p-6"> | |
| <h2 class="text-2xl font-semibold text-gray-800 mb-4 flex items-center"> | |
| <i class="fas fa-history mr-3 text-purple-600"></i> Event History | |
| </h2> | |
| <div class="event-table"> | |
| <table class="w-full"> | |
| <thead> | |
| <tr class="border-b-2 border-gray-200"> | |
| <th class="py-3 text-left">Type</th> | |
| <th class="py-3 text-left">Time</th> | |
| <th class="py-3 text-left">Date</th> | |
| <th class="py-3 text-left">Duration Since Last</th> | |
| <th class="py-3 text-left">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="event-table-body"> | |
| <!-- Events will be added here --> | |
| </tbody> | |
| </table> | |
| <p id="no-events" class="text-gray-500 text-center py-4">No events recorded yet</p> | |
| </div> | |
| </div> | |
| <!-- Stats Section --> | |
| <div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="bg-white rounded-lg shadow p-4 text-center"> | |
| <div class="text-gray-500">Today's Pee Count</div> | |
| <div class="text-3xl font-bold text-blue-600" id="today-pee-count">0</div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-4 text-center"> | |
| <div class="text-gray-500">Today's Poop Count</div> | |
| <div class="text-3xl font-bold text-amber-600" id="today-poop-count">0</div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-4 text-center"> | |
| <div class="text-gray-500">Total Events</div> | |
| <div class="text-3xl font-bold text-purple-600" id="total-events">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize variables | |
| let lastPeeTime = localStorage.getItem('lastPeeTime') ? new Date(localStorage.getItem('lastPeeTime')) : null; | |
| let lastPoopTime = localStorage.getItem('lastPoopTime') ? new Date(localStorage.getItem('lastPoopTime')) : null; | |
| let events = JSON.parse(localStorage.getItem('events')) || []; | |
| // DOM elements | |
| const peeTimer = document.getElementById('pee-timer'); | |
| const poopTimer = document.getElementById('poop-timer'); | |
| const peeBtn = document.getElementById('pee-btn'); | |
| const poopBtn = document.getElementById('poop-btn'); | |
| const peeConfirmation = document.getElementById('pee-confirmation'); | |
| const poopConfirmation = document.getElementById('poop-confirmation'); | |
| const eventTableBody = document.getElementById('event-table-body'); | |
| const noEvents = document.getElementById('no-events'); | |
| const todayPeeCount = document.getElementById('today-pee-count'); | |
| const todayPoopCount = document.getElementById('today-poop-count'); | |
| const totalEvents = document.getElementById('total-events'); | |
| // Update timers | |
| function updateTimers() { | |
| const now = new Date(); | |
| // Pee timer | |
| if (lastPeeTime) { | |
| const diff = now - lastPeeTime; | |
| peeTimer.textContent = formatTime(diff); | |
| } else { | |
| peeTimer.textContent = "No record"; | |
| } | |
| // Poop timer | |
| if (lastPoopTime) { | |
| const diff = now - lastPoopTime; | |
| poopTimer.textContent = formatTime(diff); | |
| } else { | |
| poopTimer.textContent = "No record"; | |
| } | |
| } | |
| // Format time (ms to HH:MM:SS) | |
| function formatTime(ms) { | |
| let seconds = Math.floor(ms / 1000); | |
| let hours = Math.floor(seconds / 3600); | |
| seconds %= 3600; | |
| let minutes = Math.floor(seconds / 60); | |
| seconds %= 60; | |
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| } | |
| // Add event to history | |
| function addEvent(type, time) { | |
| const now = new Date(); | |
| const event = { | |
| type: type, | |
| time: time, | |
| date: now.toLocaleDateString(), | |
| timestamp: now.getTime() | |
| }; | |
| events.unshift(event); // Add to beginning of array | |
| saveEvents(); | |
| renderEvents(); | |
| updateStats(); | |
| updateLastTimes(); | |
| } | |
| // Delete event from history | |
| function deleteEvent(index) { | |
| events.splice(index, 1); | |
| saveEvents(); | |
| renderEvents(); | |
| updateStats(); | |
| updateLastTimes(); | |
| } | |
| // Save events to localStorage | |
| function saveEvents() { | |
| localStorage.setItem('events', JSON.stringify(events)); | |
| } | |
| // Update last pee/poop times based on remaining events | |
| function updateLastTimes() { | |
| // Find most recent pee event | |
| const peeEvents = events.filter(e => e.type === 'pee'); | |
| lastPeeTime = peeEvents.length > 0 ? new Date(peeEvents[0].timestamp) : null; | |
| if (lastPeeTime) { | |
| localStorage.setItem('lastPeeTime', lastPeeTime); | |
| } else { | |
| localStorage.removeItem('lastPeeTime'); | |
| } | |
| // Find most recent poop event | |
| const poopEvents = events.filter(e => e.type === 'poop'); | |
| lastPoopTime = poopEvents.length > 0 ? new Date(poopEvents[0].timestamp) : null; | |
| if (lastPoopTime) { | |
| localStorage.setItem('lastPoopTime', lastPoopTime); | |
| } else { | |
| localStorage.removeItem('lastPoopTime'); | |
| } | |
| updateTimers(); | |
| } | |
| // Render events in table | |
| function renderEvents() { | |
| if (events.length === 0) { | |
| noEvents.classList.remove('hidden'); | |
| eventTableBody.innerHTML = ''; | |
| return; | |
| } | |
| noEvents.classList.add('hidden'); | |
| eventTableBody.innerHTML = ''; | |
| events.forEach((event, index) => { | |
| const row = document.createElement('tr'); | |
| row.className = index % 2 === 0 ? 'bg-gray-50' : 'bg-white'; | |
| // Calculate duration since last event of same type | |
| let durationText = 'First record'; | |
| if (index > 0) { | |
| // Find previous event of the same type | |
| const prevEventIndex = events.slice(index + 1).findIndex(e => e.type === event.type); | |
| if (prevEventIndex !== -1) { | |
| const prevEvent = events[index + 1 + prevEventIndex]; | |
| const duration = event.timestamp - prevEvent.timestamp; | |
| durationText = formatTime(duration); | |
| } | |
| } | |
| row.innerHTML = ` | |
| <td class="py-3"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ | |
| event.type === 'pee' ? 'bg-blue-100 text-blue-800' : 'bg-amber-100 text-amber-800' | |
| }"> | |
| ${event.type === 'pee' ? 'Pee' : 'Poop'} | |
| </span> | |
| </td> | |
| <td class="py-3">${event.time}</td> | |
| <td class="py-3">${event.date}</td> | |
| <td class="py-3">${durationText}</td> | |
| <td class="py-3"> | |
| <button class="delete-btn text-gray-400 hover:text-red-500" data-index="${index}"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| </td> | |
| `; | |
| eventTableBody.appendChild(row); | |
| }); | |
| // Add event listeners to delete buttons | |
| document.querySelectorAll('.delete-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const index = parseInt(btn.getAttribute('data-index')); | |
| if (confirm('Are you sure you want to delete this event?')) { | |
| deleteEvent(index); | |
| } | |
| }); | |
| }); | |
| } | |
| // Update statistics | |
| function updateStats() { | |
| const today = new Date().toLocaleDateString(); | |
| const peeCount = events.filter(e => e.type === 'pee' && e.date === today).length; | |
| const poopCount = events.filter(e => e.type === 'poop' && e.date === today).length; | |
| todayPeeCount.textContent = peeCount; | |
| todayPoopCount.textContent = poopCount; | |
| totalEvents.textContent = events.length; | |
| } | |
| // Event listeners | |
| peeBtn.addEventListener('click', () => { | |
| const now = new Date(); | |
| const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| lastPeeTime = now; | |
| localStorage.setItem('lastPeeTime', lastPeeTime); | |
| addEvent('pee', timeString); | |
| // Show confirmation | |
| peeConfirmation.classList.remove('hidden'); | |
| setTimeout(() => { | |
| peeConfirmation.classList.add('hidden'); | |
| }, 2000); | |
| }); | |
| poopBtn.addEventListener('click', () => { | |
| const now = new Date(); | |
| const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| lastPoopTime = now; | |
| localStorage.setItem('lastPoopTime', lastPoopTime); | |
| addEvent('poop', timeString); | |
| // Show confirmation | |
| poopConfirmation.classList.remove('hidden'); | |
| setTimeout(() => { | |
| poopConfirmation.classList.add('hidden'); | |
| }, 2000); | |
| }); | |
| // Initialize | |
| updateTimers(); | |
| renderEvents(); | |
| updateStats(); | |
| // Update timers every second | |
| setInterval(updateTimers, 1000); | |
| </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=cmauck10/doggy-tracker" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |