Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Weight Tracker</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> | |
| <style> | |
| .tooltip { | |
| position: absolute; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| z-index: 1000; | |
| } | |
| .chart-container:hover .tooltip { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="text-center mb-12 relative" data-aos="fade-down"> | |
| <div class="absolute top-0 right-0 flex items-center space-x-3"> | |
| <div class="flex items-center space-x-3"> | |
| <div id="userInfo" style="display: none;" class="flex flex-row items-center gap-2 flex-nowrap"> | |
| <div class="bg-indigo-100 text-indigo-700 px-4 py-2 rounded-full font-medium inline-flex items-center"> | |
| <i data-feather="user" class="mr-2"></i> | |
| <span id="userName">User</span> | |
| </div> | |
| <button onclick="signOut()" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-full font-medium hover:bg-gray-300 transition duration-200 inline-flex items-center"> | |
| <i data-feather="log-out" class="mr-2"></i>Logout | |
| </button> | |
| </div> | |
| <button onclick="showLogin()" id="loginButton" class="bg-indigo-600 text-white font-medium hover:bg-indigo-700 transition duration-200 inline-flex items-center gap-2 px-4 py-2 rounded-full whitespace-nowrap"> | |
| <i data-feather="log-in" class="w-4 h-4"></i>Login | |
| </button> | |
| </div> | |
| </div> | |
| <h1 class="text-4xl font-bold text-indigo-700 mb-2">Weight Tracker</h1> | |
| <p class="text-gray-600">Visualizing your health journey</p> | |
| </header> | |
| <!-- Login Modal --> | |
| <div id="loginModal" style="display: none;" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> | |
| <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"> | |
| <div class="mt-3 text-center"> | |
| <h3 class="text-lg font-medium text-gray-900">Login</h3> | |
| <div class="mt-2 px-7 py-3"> | |
| <input type="email" id="loginEmail" placeholder="Email" class="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <input type="password" id="loginPassword" placeholder="Password" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div class="text-sm text-center mb-4"> | |
| <a href="#" onclick="showSignup()" class="text-indigo-600 hover:text-indigo-800">Create an account</a> | |
| </div> | |
| <div class="flex items-center justify-between px-4 py-3"> | |
| <button onclick="closeLogin()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"> | |
| Cancel | |
| </button> | |
| <button onclick="handleLogin()" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"> | |
| Login | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Signup Modal --> | |
| <div id="signupModal" style="display: none;" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> | |
| <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"> | |
| <div class="mt-3 text-center"> | |
| <h3 class="text-lg font-medium text-gray-900">Create Account</h3> | |
| <div class="mt-2 px-7 py-3"> | |
| <input type="text" id="signupName" placeholder="Username" class="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <input type="email" id="signupEmail" placeholder="Email" class="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <input type="password" id="signupPassword" placeholder="Password" class="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <input type="password" id="signupConfirmPassword" placeholder="Confirm Password" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div class="text-sm text-center mb-4"> | |
| <a href="#" onclick="showLogin()" class="text-indigo-600 hover:text-indigo-800">Already have an account? Login</a> | |
| </div> | |
| <div class="flex items-center justify-between px-4 py-3"> | |
| <button onclick="closeSignup()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"> | |
| Cancel | |
| </button> | |
| <button onclick="handleSignup()" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"> | |
| Sign Up | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-lg p-6 mb-8" data-aos="fade-up"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-semibold text-gray-800">Progress Chart</h2> | |
| <div class="flex space-x-2"> | |
| <button id="weekBtn" class="px-4 py-2 bg-indigo-100 text-indigo-700 rounded-lg font-medium">Week</button> | |
| <button id="monthBtn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium">Month</button> | |
| <button id="yearBtn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium">Year</button> | |
| </div> | |
| </div> | |
| <div class="chart-container relative h-96"> | |
| <canvas id="weightChart"></canvas> | |
| <div class="tooltip" id="chartTooltip"></div> | |
| </div> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="bg-white rounded-xl shadow-lg p-6" data-aos="fade-right"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Add New Entry</h2> | |
| <form id="weightForm" class="space-y-4"> | |
| <div> | |
| <label for="date" class="block text-sm font-medium text-gray-700 mb-1">Date (DD/MM/YY)</label> | |
| <input type="text" id="date" placeholder="DD/MM/YY" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="weight" class="block text-sm font-medium text-gray-700 mb-1">Weight (kg)</label> | |
| <input type="number" step="0.1" id="weight" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-lg hover:bg-indigo-700 transition duration-200 flex items-center justify-center"> | |
| <i data-feather="plus" class="mr-2"></i> Add Entry | |
| </button> | |
| </form> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-lg p-6" data-aos="fade-left"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Recent Entries</h2> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Weight</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Change</th> | |
| </tr> | |
| </thead> | |
| <tbody id="entriesTable" class="bg-white divide-y divide-gray-200"> | |
| <!-- Entries will be added here dynamically --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize AOS and Feather Icons | |
| AOS.init({ | |
| duration: 800, | |
| easing: 'ease-in-out' | |
| }); | |
| feather.replace(); | |
| // Login functionality | |
| function handleLogin() { | |
| const email = document.getElementById('loginEmail').value; | |
| const password = document.getElementById('loginPassword').value; | |
| if (!email || !password) { | |
| alert('Please enter both email and password'); | |
| return; | |
| } | |
| // Simple validation | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!emailRegex.test(email)) { | |
| alert('Please enter a valid email address'); | |
| return; | |
| } | |
| // Check if user exists | |
| const users = JSON.parse(localStorage.getItem('users') || '[]'); | |
| const user = users.find(u => u.email === email && u.password === password); | |
| if (!user) { | |
| alert('Invalid email or password'); | |
| return; | |
| } | |
| // Store user info in localStorage | |
| localStorage.setItem('user', JSON.stringify({ | |
| name: user.name, | |
| email: user.email | |
| })); | |
| // Update UI | |
| document.getElementById('userName').textContent = user.name; | |
| document.getElementById('loginModal').style.display = 'none'; | |
| document.getElementById('userInfo').style.display = 'block'; | |
| document.getElementById('loginButton').style.display = 'none'; | |
| // Refresh the page to load user data | |
| window.location.reload(); | |
| } | |
| // Signup functionality | |
| function handleSignup() { | |
| const name = document.getElementById('signupName').value; | |
| const email = document.getElementById('signupEmail').value; | |
| const password = document.getElementById('signupPassword').value; | |
| const confirmPassword = document.getElementById('signupConfirmPassword').value; | |
| if (!name || !email || !password || !confirmPassword) { | |
| alert('Please fill in all fields'); | |
| return; | |
| } | |
| if (password !== confirmPassword) { | |
| alert('Passwords do not match'); | |
| return; | |
| } | |
| // Email validation | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!emailRegex.test(email)) { | |
| alert('Please enter a valid email address'); | |
| return; | |
| } | |
| // Check if user already exists | |
| const users = JSON.parse(localStorage.getItem('users') || '[]'); | |
| if (users.some(u => u.email === email)) { | |
| alert('Email already registered. Please login instead.'); | |
| return; | |
| } | |
| // Add new user with pending confirmation | |
| users.push({ | |
| name: name, | |
| email: email, | |
| password: password, | |
| confirmed: false | |
| }); | |
| localStorage.setItem('users', JSON.stringify(users)); | |
| // Show confirmation message | |
| document.getElementById('signupModal').style.display = 'none'; | |
| alert('Account created! A confirmation email has been sent to ' + email + '. Please check your email to confirm your account.'); | |
| } | |
| // Initialize the app | |
| window.onload = function () { | |
| // Check if user is already logged in | |
| const savedUser = localStorage.getItem('user'); | |
| if (savedUser) { | |
| try { | |
| const user = JSON.parse(savedUser); | |
| document.getElementById('userName').textContent = user.name; | |
| document.getElementById('userInfo').style.display = 'block'; | |
| document.getElementById('loginButton').style.display = 'none'; | |
| // Load user-specific data | |
| weightData = getUserWeightData(); | |
| calculateChanges(); | |
| renderEntriesTable(); | |
| renderChart(); | |
| } catch (error) { | |
| console.error("Error loading saved user:", error); | |
| localStorage.removeItem('user'); | |
| } | |
| } else { | |
| // If no user is logged in, clear any previous data | |
| weightData = []; | |
| calculateChanges(); | |
| renderEntriesTable(); | |
| renderChart(); | |
| } | |
| }; | |
| // Load data from localStorage for current user or use empty array | |
| function getUserWeightData() { | |
| const savedUser = localStorage.getItem('user'); | |
| if (!savedUser) return []; | |
| try { | |
| const user = JSON.parse(savedUser); | |
| const allUserData = JSON.parse(localStorage.getItem('userWeightData') || '{}'); | |
| return allUserData[user.email] || []; | |
| } catch (error) { | |
| console.error("Error loading user data:", error); | |
| return []; | |
| } | |
| } | |
| let weightData = getUserWeightData(); | |
| // Sort data by date | |
| weightData.sort((a, b) => { | |
| const [dayA, monthA, yearA] = a.date.split('/').map(Number); | |
| const [dayB, monthB, yearB] = b.date.split('/').map(Number); | |
| const dateA = new Date(2000 + yearA, monthA - 1, dayA); | |
| const dateB = new Date(2000 + yearB, monthB - 1, dayB); | |
| return dateA - dateB; | |
| }); | |
| // Save data to localStorage for current user | |
| function saveData() { | |
| const savedUser = localStorage.getItem('user'); | |
| if (!savedUser) return; | |
| try { | |
| const user = JSON.parse(savedUser); | |
| const allUserData = JSON.parse(localStorage.getItem('userWeightData') || '{}'); | |
| allUserData[user.email] = weightData; | |
| localStorage.setItem('userWeightData', JSON.stringify(allUserData)); | |
| } catch (error) { | |
| console.error("Error saving user data:", error); | |
| } | |
| } | |
| // Format dates for display | |
| function formatDate(dateStr) { | |
| // Already in DD/MM/YY format | |
| return dateStr; | |
| } | |
| // Format dates for chart display | |
| function formatChartDate(dateStr) { | |
| const parts = dateStr.split('/'); | |
| const day = parts[0]; | |
| const month = parts[1]; | |
| return `${day}/${month}`; | |
| } | |
| // Calculate changes between entries | |
| function calculateChanges() { | |
| for (let i = 1; i < weightData.length; i++) { | |
| const change = weightData[i].weight - weightData[i-1].weight; | |
| weightData[i].change = change.toFixed(1); | |
| } | |
| if (weightData.length > 0) { | |
| weightData[0].change = '—'; | |
| } | |
| } | |
| // Render the table with entries | |
| function renderEntriesTable() { | |
| const tableBody = document.getElementById('entriesTable'); | |
| tableBody.innerHTML = ''; | |
| weightData.forEach(entry => { | |
| const row = document.createElement('tr'); | |
| const changeClass = entry.change === '—' ? '' : | |
| parseFloat(entry.change) < 0 ? 'text-green-500' : 'text-red-500'; | |
| const changeSymbol = entry.change === '—' ? '' : | |
| parseFloat(entry.change) < 0 ? '↓' : '↑'; | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${formatDate(entry.date)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.weight} kg</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm ${changeClass}"> | |
| ${changeSymbol} ${entry.change === '—' ? '' : Math.abs(entry.change)} kg | |
| </td> | |
| `; | |
| tableBody.appendChild(row); | |
| }); | |
| } | |
| // Initialize and render the chart | |
| let weightChart = null; | |
| function renderChart() { | |
| const ctx = document.getElementById('weightChart').getContext('2d'); | |
| // Destroy existing chart if it exists | |
| if (weightChart) { | |
| weightChart.destroy(); | |
| } | |
| // Prepare chart data | |
| const labels = weightData.map(entry => formatChartDate(entry.date)); | |
| const data = weightData.map(entry => entry.weight); | |
| // Calculate goal line (target is 85kg) | |
| const goalLine = Array(weightData.length).fill(85); | |
| weightChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [ | |
| { | |
| label: 'Your Weight', | |
| data: data, | |
| borderColor: 'rgb(79, 70, 229)', | |
| backgroundColor: 'rgba(79, 70, 229, 0.1)', | |
| borderWidth: 3, | |
| tension: 0.3, | |
| fill: true, | |
| pointBackgroundColor: 'rgb(79, 70, 229)', | |
| pointRadius: 5, | |
| pointHoverRadius: 7 | |
| }, | |
| { | |
| label: 'Goal (85kg)', | |
| data: goalLine, | |
| borderColor: 'rgb(239, 68, 68)', | |
| borderWidth: 2, | |
| borderDash: [5, 5], | |
| pointRadius: 0, | |
| fill: false | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: false, | |
| title: { | |
| display: true, | |
| text: 'Weight (kg)' | |
| } | |
| }, | |
| x: { | |
| title: { | |
| display: true, | |
| text: 'Date' | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| }, | |
| tooltip: { | |
| enabled: false, | |
| external: function(context) { | |
| const tooltip = document.getElementById('chartTooltip'); | |
| if (context.tooltip.opacity === 0) { | |
| tooltip.style.opacity = 0; | |
| return; | |
| } | |
| const dataIndex = context.tooltip.dataPoints[0].dataIndex; | |
| const weight = weightData[dataIndex].weight; | |
| const date = formatDate(weightData[dataIndex].date); | |
| tooltip.innerHTML = ` | |
| <div class="font-bold">${date}</div> | |
| <div>Weight: ${weight} kg</div> | |
| `; | |
| tooltip.style.left = context.tooltip.caretX + 'px'; | |
| tooltip.style.top = context.tooltip.caretY + 'px'; | |
| tooltip.style.opacity = 1; | |
| } | |
| } | |
| }, | |
| interaction: { | |
| intersect: false, | |
| mode: 'index' | |
| } | |
| } | |
| }); | |
| } | |
| // Handle form submission | |
| document.getElementById('weightForm').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const dateInput = document.getElementById('date').value; | |
| const weight = parseFloat(document.getElementById('weight').value); | |
| // Validate date format DD/MM/YY | |
| const dateRegex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/([0-9]{2})$/; | |
| if (!dateRegex.test(dateInput)) { | |
| alert('Please enter date in DD/MM/YY format (e.g. 25/12/23)'); | |
| return; | |
| } | |
| if (isNaN(weight) || weight <= 0) { | |
| alert('Please enter a valid weight in kg'); | |
| return; | |
| } | |
| weightData.push({ date: dateInput, weight }); | |
| // Sort data chronologically after adding new entry | |
| weightData.sort((a, b) => { | |
| const [dayA, monthA, yearA] = a.date.split('/').map(Number); | |
| const [dayB, monthB, yearB] = b.date.split('/').map(Number); | |
| const dateA = new Date(2000 + yearA, monthA - 1, dayA); | |
| const dateB = new Date(2000 + yearB, monthB - 1, dayB); | |
| return dateA - dateB; | |
| }); | |
| calculateChanges(); | |
| saveData(); // Save to localStorage | |
| renderEntriesTable(); | |
| renderChart(); | |
| // Reset form | |
| document.getElementById('weightForm').reset(); | |
| }); | |
| // Sign out function | |
| function signOut() { | |
| localStorage.removeItem('user'); | |
| // Force full page refresh to reset everything | |
| window.location.reload(); | |
| } | |
| // Show login modal | |
| function showLogin() { | |
| document.getElementById('loginModal').style.display = 'block'; | |
| document.getElementById('signupModal').style.display = 'none'; | |
| } | |
| // Show signup modal | |
| function showSignup() { | |
| document.getElementById('loginModal').style.display = 'none'; | |
| document.getElementById('signupModal').style.display = 'block'; | |
| } | |
| // Close login modal | |
| function closeLogin() { | |
| document.getElementById('loginModal').style.display = 'none'; | |
| } | |
| // Close signup modal | |
| function closeSignup() { | |
| document.getElementById('signupModal').style.display = 'none'; | |
| } | |
| // Initialize the app | |
| calculateChanges(); | |
| renderEntriesTable(); | |
| renderChart(); | |
| // Time period buttons functionality | |
| document.getElementById('weekBtn').addEventListener('click', function() { | |
| // In a real app, this would filter data for the week view | |
| alert('Week view selected. In a complete app, this would filter the data.'); | |
| }); | |
| document.getElementById('monthBtn').addEventListener('click', function() { | |
| // In a real app, this would filter data for the month view | |
| alert('Month view selected. In a complete app, this would filter the data.'); | |
| }); | |
| document.getElementById('yearBtn').addEventListener('click', function() { | |
| // In a real app, this would filter data for the year view | |
| alert('Year view selected. In a complete app, this would filter the data.'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |