| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ZenHabit | Minimal Habit Tracker</title> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| :root { |
| --primary: #6366f1; |
| --primary-light: #818cf8; |
| --text: #1e293b; |
| --text-light: #64748b; |
| --bg: #f8fafc; |
| --card: #ffffff; |
| --border: #e2e8f0; |
| --shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| --success: #10b981; |
| --warning: #f59e0b; |
| --danger: #ef4444; |
| } |
| |
| .dark-mode { |
| --primary: #818cf8; |
| --primary-light: #a5b4fc; |
| --text: #e2e8f0; |
| --text-light: #94a3b8; |
| --bg: #0f172a; |
| --card: #1e293b; |
| --border: #334155; |
| --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| transition: background-color 0.3s, color 0.3s; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background-color: var(--bg); |
| color: var(--text); |
| line-height: 1.6; |
| } |
| |
| .container { |
| max-width: 800px; |
| margin: 0 auto; |
| padding: 2rem; |
| } |
| |
| header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 2rem; |
| } |
| |
| h1 { |
| font-size: 1.8rem; |
| font-weight: 700; |
| color: var(--primary); |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| .header-controls { |
| display: flex; |
| gap: 0.5rem; |
| align-items: center; |
| } |
| |
| .icon-btn { |
| background: none; |
| border: none; |
| color: var(--text-light); |
| font-size: 1.2rem; |
| cursor: pointer; |
| transition: all 0.3s; |
| padding: 0.25rem; |
| } |
| |
| .icon-btn:hover { |
| color: var(--primary); |
| transform: rotate(15deg); |
| } |
| |
| .stats { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 1rem; |
| margin-bottom: 2rem; |
| } |
| |
| .stat-card { |
| background-color: var(--card); |
| border-radius: 0.5rem; |
| padding: 1rem; |
| box-shadow: var(--shadow); |
| text-align: center; |
| } |
| |
| .stat-card h3 { |
| font-size: 0.9rem; |
| color: var(--text-light); |
| margin-bottom: 0.5rem; |
| } |
| |
| .stat-card p { |
| font-size: 1.5rem; |
| font-weight: 700; |
| color: var(--primary); |
| } |
| |
| .habits { |
| margin-bottom: 2rem; |
| } |
| |
| .habits-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1rem; |
| } |
| |
| .habits-header h2 { |
| font-size: 1.3rem; |
| } |
| |
| .add-habit { |
| background-color: var(--primary); |
| color: white; |
| border: none; |
| border-radius: 0.3rem; |
| padding: 0.5rem 1rem; |
| font-size: 0.9rem; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| transition: background-color 0.3s; |
| } |
| |
| .add-habit:hover { |
| background-color: var(--primary-light); |
| } |
| |
| .habit-list { |
| display: flex; |
| flex-direction: column; |
| gap: 0.5rem; |
| } |
| |
| .habit-item { |
| background-color: var(--card); |
| border-radius: 0.5rem; |
| padding: 1rem; |
| box-shadow: var(--shadow); |
| display: flex; |
| align-items: center; |
| gap: 1rem; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .habit-item::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| height: 100%; |
| width: 0.3rem; |
| background-color: var(--primary); |
| } |
| |
| .habit-check { |
| width: 1.5rem; |
| height: 1.5rem; |
| border: 2px solid var(--border); |
| border-radius: 0.3rem; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: all 0.3s; |
| } |
| |
| .habit-check.checked { |
| background-color: var(--primary); |
| border-color: var(--primary); |
| color: white; |
| } |
| |
| .habit-info { |
| flex: 1; |
| } |
| |
| .habit-name { |
| font-weight: 600; |
| margin-bottom: 0.2rem; |
| } |
| |
| .habit-streak { |
| font-size: 0.8rem; |
| color: var(--text-light); |
| display: flex; |
| align-items: center; |
| gap: 0.3rem; |
| } |
| |
| .habit-streak i { |
| color: var(--warning); |
| } |
| |
| .habit-progress { |
| width: 100px; |
| height: 0.3rem; |
| background-color: var(--border); |
| border-radius: 1rem; |
| overflow: hidden; |
| } |
| |
| .progress-bar { |
| height: 100%; |
| background-color: var(--primary); |
| border-radius: 1rem; |
| transition: width 0.5s ease; |
| } |
| |
| .calendar { |
| background-color: var(--card); |
| border-radius: 0.5rem; |
| padding: 1rem; |
| box-shadow: var(--shadow); |
| } |
| |
| .calendar-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1rem; |
| } |
| |
| .calendar-nav { |
| display: flex; |
| gap: 1rem; |
| } |
| |
| .calendar-nav button { |
| background: none; |
| border: none; |
| color: var(--text-light); |
| cursor: pointer; |
| font-size: 1rem; |
| } |
| |
| .calendar-nav button:hover { |
| color: var(--primary); |
| } |
| |
| .calendar-grid { |
| display: grid; |
| grid-template-columns: repeat(7, 1fr); |
| gap: 0.5rem; |
| } |
| |
| .calendar-day-header { |
| text-align: center; |
| font-size: 0.8rem; |
| color: var(--text-light); |
| padding: 0.5rem 0; |
| } |
| |
| .calendar-day { |
| aspect-ratio: 1; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| border-radius: 0.3rem; |
| cursor: pointer; |
| position: relative; |
| } |
| |
| .calendar-day:hover { |
| background-color: var(--border); |
| } |
| |
| .calendar-day.today { |
| background-color: var(--primary); |
| color: white; |
| } |
| |
| .day-number { |
| font-size: 0.9rem; |
| font-weight: 500; |
| } |
| |
| .day-habits { |
| position: absolute; |
| bottom: 0.2rem; |
| display: flex; |
| gap: 0.2rem; |
| } |
| |
| .day-habit-dot { |
| width: 0.3rem; |
| height: 0.3rem; |
| border-radius: 50%; |
| background-color: var(--success); |
| } |
| |
| .modal { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.5); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| z-index: 1000; |
| opacity: 0; |
| pointer-events: none; |
| transition: opacity 0.3s; |
| } |
| |
| .modal.active { |
| opacity: 1; |
| pointer-events: all; |
| } |
| |
| .modal-content { |
| background-color: var(--card); |
| border-radius: 0.5rem; |
| padding: 1.5rem; |
| width: 90%; |
| max-width: 400px; |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); |
| transform: translateY(-20px); |
| transition: transform 0.3s; |
| } |
| |
| .modal.active .modal-content { |
| transform: translateY(0); |
| } |
| |
| .modal-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1rem; |
| } |
| |
| .modal-header h3 { |
| font-size: 1.2rem; |
| } |
| |
| .close-modal { |
| background: none; |
| border: none; |
| font-size: 1.2rem; |
| color: var(--text-light); |
| cursor: pointer; |
| } |
| |
| .form-group { |
| margin-bottom: 1rem; |
| } |
| |
| .form-group label { |
| display: block; |
| margin-bottom: 0.5rem; |
| font-size: 0.9rem; |
| color: var(--text-light); |
| } |
| |
| .form-group input, |
| .form-group select { |
| width: 100%; |
| padding: 0.5rem; |
| border: 1px solid var(--border); |
| border-radius: 0.3rem; |
| background-color: var(--bg); |
| color: var(--text); |
| } |
| |
| .modal-actions { |
| display: flex; |
| justify-content: flex-end; |
| gap: 0.5rem; |
| margin-top: 1rem; |
| } |
| |
| .btn { |
| padding: 0.5rem 1rem; |
| border-radius: 0.3rem; |
| cursor: pointer; |
| font-size: 0.9rem; |
| border: none; |
| transition: background-color 0.3s; |
| } |
| |
| .btn-primary { |
| background-color: var(--primary); |
| color: white; |
| } |
| |
| .btn-primary:hover { |
| background-color: var(--primary-light); |
| } |
| |
| .btn-secondary { |
| background-color: var(--border); |
| color: var(--text); |
| } |
| |
| .btn-secondary:hover { |
| background-color: #d1d5db; |
| } |
| |
| @media (max-width: 600px) { |
| .stats { |
| grid-template-columns: 1fr; |
| } |
| |
| .container { |
| padding: 1rem; |
| } |
| } |
| |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.1); } |
| 100% { transform: scale(1); } |
| } |
| |
| .habit-check.checked { |
| animation: pulse 0.3s ease; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1><i class="fas fa-leaf"></i> ZenHabit</h1> |
| <div class="header-controls"> |
| <button class="icon-btn" id="exportBtn" title="Export Data"> |
| <i class="fas fa-file-export"></i> |
| </button> |
| <button class="icon-btn" id="importBtn" title="Import Data"> |
| <i class="fas fa-file-import"></i> |
| </button> |
| <button class="icon-btn" id="themeToggle" title="Toggle Theme"> |
| <i class="fas fa-moon"></i> |
| </button> |
| </div> |
| </header> |
|
|
| <div class="stats"> |
| <div class="stat-card"> |
| <h3>Current Streak</h3> |
| <p id="currentStreak">0</p> |
| </div> |
| <div class="stat-card"> |
| <h3>Habits Tracked</h3> |
| <p id="habitsTracked">0</p> |
| </div> |
| <div class="stat-card"> |
| <h3>Completion Rate</h3> |
| <p id="completionRate">0%</p> |
| </div> |
| </div> |
|
|
| <div class="habits"> |
| <div class="habits-header"> |
| <h2>Today's Habits</h2> |
| <button class="add-habit" id="addHabitBtn"> |
| <i class="fas fa-plus"></i> Add Habit |
| </button> |
| </div> |
| <div class="habit-list" id="habitList"> |
| |
| </div> |
| </div> |
|
|
| <div class="calendar"> |
| <div class="calendar-header"> |
| <h3 id="currentMonth"></h3> |
| <div class="calendar-nav"> |
| <button id="prevMonth"><i class="fas fa-chevron-left"></i></button> |
| <button id="nextMonth"><i class="fas fa-chevron-right"></i></button> |
| </div> |
| </div> |
| <div class="calendar-grid" id="calendarGrid"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="addHabitModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3>Add New Habit</h3> |
| <button class="close-modal" id="closeModal">×</button> |
| </div> |
| <form id="habitForm"> |
| <div class="form-group"> |
| <label for="habitName">Habit Name</label> |
| <input type="text" id="habitName" placeholder="e.g. Drink water" required> |
| </div> |
| <div class="form-group"> |
| <label for="habitFrequency">Frequency</label> |
| <select id="habitFrequency" required> |
| <option value="daily">Daily</option> |
| <option value="weekly">Weekly</option> |
| <option value="monthly">Monthly</option> |
| </select> |
| </div> |
| <div class="modal-actions"> |
| <button type="button" class="btn btn-secondary" id="cancelHabit">Cancel</button> |
| <button type="submit" class="btn btn-primary">Add Habit</button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| <input type="file" id="fileInput" hidden accept=".json"> |
|
|
| <script> |
| let habits = []; |
| let currentDate = new Date(); |
| let currentYear = currentDate.getFullYear(); |
| let currentMonthIndex = currentDate.getMonth(); |
| |
| |
| const themeToggle = document.getElementById('themeToggle'); |
| const addHabitBtn = document.getElementById('addHabitBtn'); |
| const addHabitModal = document.getElementById('addHabitModal'); |
| const closeModal = document.getElementById('closeModal'); |
| const cancelHabit = document.getElementById('cancelHabit'); |
| const habitForm = document.getElementById('habitForm'); |
| const habitList = document.getElementById('habitList'); |
| const currentStreak = document.getElementById('currentStreak'); |
| const habitsTracked = document.getElementById('habitsTracked'); |
| const completionRate = document.getElementById('completionRate'); |
| const currentMonth = document.getElementById('currentMonth'); |
| const calendarGrid = document.getElementById('calendarGrid'); |
| const prevMonthBtn = document.getElementById('prevMonth'); |
| const nextMonthBtn = document.getElementById('nextMonth'); |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| loadData(); |
| renderHabits(); |
| renderCalendar(); |
| updateStats(); |
| |
| if (localStorage.getItem('theme') === 'dark') { |
| document.body.classList.add('dark-mode'); |
| themeToggle.innerHTML = '<i class="fas fa-sun"></i>'; |
| } |
| }); |
| |
| |
| function loadData() { |
| const savedData = localStorage.getItem('habits'); |
| if (savedData) { |
| try { |
| habits = JSON.parse(savedData); |
| } catch (e) { |
| console.error('Error loading data:', e); |
| } |
| } |
| } |
| |
| function saveData() { |
| localStorage.setItem('habits', JSON.stringify(habits)); |
| } |
| |
| |
| document.getElementById('exportBtn').addEventListener('click', exportData); |
| document.getElementById('importBtn').addEventListener('click', () => document.getElementById('fileInput').click()); |
| document.getElementById('fileInput').addEventListener('change', importData); |
| |
| function exportData() { |
| const data = JSON.stringify(habits, null, 2); |
| const blob = new Blob([data], { type: 'application/json' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `zenhabit-${new Date().toISOString().split('T')[0]}.json`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| } |
| |
| function importData(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = (event) => { |
| try { |
| const imported = JSON.parse(event.target.result); |
| habits = imported; |
| saveData(); |
| renderHabits(); |
| updateStats(); |
| renderCalendar(); |
| } catch (error) { |
| alert('Invalid file format. Please import a valid JSON file.'); |
| } |
| }; |
| reader.readAsText(file); |
| } |
| |
| |
| function getNextId() { |
| return habits.length > 0 ? Math.max(...habits.map(h => h.id)) + 1 : 1; |
| } |
| |
| habitForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| const name = document.getElementById('habitName').value.trim(); |
| const frequency = document.getElementById('habitFrequency').value; |
| |
| if (!name) return; |
| |
| habits.push({ |
| id: getNextId(), |
| name, |
| streak: 0, |
| frequency, |
| progress: 0, |
| checked: false |
| }); |
| |
| saveData(); |
| renderHabits(); |
| updateStats(); |
| habitForm.reset(); |
| addHabitModal.classList.remove('active'); |
| }); |
| |
| function renderHabits() { |
| habitList.innerHTML = habits.map(habit => ` |
| <div class="habit-item"> |
| <div class="habit-check ${habit.checked ? 'checked' : ''}" data-id="${habit.id}"> |
| ${habit.checked ? '<i class="fas fa-check"></i>' : ''} |
| </div> |
| <div class="habit-info"> |
| <div class="habit-name">${habit.name}</div> |
| <div class="habit-streak"> |
| <i class="fas fa-fire"></i> ${habit.streak} day streak |
| </div> |
| </div> |
| <div class="habit-progress"> |
| <div class="progress-bar" style="width: ${habit.progress}%"></div> |
| </div> |
| </div> |
| `).join(''); |
| |
| document.querySelectorAll('.habit-check').forEach(checkbox => { |
| checkbox.addEventListener('click', function() { |
| const habit = habits.find(h => h.id === parseInt(this.dataset.id)); |
| habit.checked = !habit.checked; |
| habit.streak = habit.checked ? habit.streak + 1 : Math.max(habit.streak - 1, 0); |
| habit.progress = Math.min(Math.max(habit.progress + (habit.checked ? 20 : -20), 0), 100); |
| saveData(); |
| updateStats(); |
| }); |
| }); |
| } |
| |
| function updateStats() { |
| const total = habits.length; |
| const completed = habits.filter(h => h.checked).length; |
| const rate = total > 0 ? Math.round((completed / total) * 100) : 0; |
| const maxStreak = habits.reduce((max, h) => Math.max(max, h.streak), 0); |
| |
| currentStreak.textContent = maxStreak; |
| habitsTracked.textContent = total; |
| completionRate.textContent = `${rate}%`; |
| } |
| |
| |
| function renderCalendar() { |
| calendarGrid.innerHTML = ''; |
| const monthName = new Date(currentYear, currentMonthIndex).toLocaleString('default', { month: 'long', year: 'numeric' }); |
| currentMonth.textContent = monthName; |
| |
| |
| ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].forEach(day => { |
| const header = document.createElement('div'); |
| header.className = 'calendar-day-header'; |
| header.textContent = day; |
| calendarGrid.appendChild(header); |
| }); |
| |
| |
| const firstDay = new Date(currentYear, currentMonthIndex, 1).getDay(); |
| const daysInMonth = new Date(currentYear, currentMonthIndex + 1, 0).getDate(); |
| |
| for (let i = 0; i < firstDay; i++) { |
| calendarGrid.appendChild(createEmptyDay()); |
| } |
| |
| for (let day = 1; day <= daysInMonth; day++) { |
| calendarGrid.appendChild(createCalendarDay(day)); |
| } |
| } |
| |
| function createEmptyDay() { |
| const day = document.createElement('div'); |
| day.className = 'calendar-day'; |
| return day; |
| } |
| |
| function createCalendarDay(dayNumber) { |
| const day = document.createElement('div'); |
| day.className = 'calendar-day'; |
| |
| if (new Date(currentYear, currentMonthIndex, dayNumber).toDateString() === new Date().toDateString()) { |
| day.classList.add('today'); |
| } |
| |
| const number = document.createElement('div'); |
| number.className = 'day-number'; |
| number.textContent = dayNumber; |
| day.appendChild(number); |
| |
| |
| const completedHabits = Math.random() > 0.5 ? Math.floor(Math.random() * 3) + 1 : 0; |
| if (completedHabits > 0) { |
| const dots = document.createElement('div'); |
| dots.className = 'day-habits'; |
| for (let i = 0; i < completedHabits; i++) { |
| const dot = document.createElement('div'); |
| dot.className = 'day-habit-dot'; |
| dots.appendChild(dot); |
| } |
| day.appendChild(dots); |
| } |
| |
| return day; |
| } |
| |
| |
| prevMonthBtn.addEventListener('click', () => { |
| currentMonthIndex--; |
| if (currentMonthIndex < 0) { |
| currentMonthIndex = 11; |
| currentYear--; |
| } |
| renderCalendar(); |
| }); |
| |
| nextMonthBtn.addEventListener('click', () => { |
| currentMonthIndex++; |
| if (currentMonthIndex > 11) { |
| currentMonthIndex = 0; |
| currentYear++; |
| } |
| renderCalendar(); |
| }); |
| |
| |
| themeToggle.addEventListener('click', () => { |
| document.body.classList.toggle('dark-mode'); |
| if (document.body.classList.contains('dark-mode')) { |
| localStorage.setItem('theme', 'dark'); |
| themeToggle.innerHTML = '<i class="fas fa-sun"></i>'; |
| } else { |
| localStorage.setItem('theme', 'light'); |
| themeToggle.innerHTML = '<i class="fas fa-moon"></i>'; |
| } |
| }); |
| |
| |
| addHabitBtn.addEventListener('click', () => addHabitModal.classList.add('active')); |
| closeModal.addEventListener('click', () => addHabitModal.classList.remove('active')); |
| cancelHabit.addEventListener('click', () => addHabitModal.classList.remove('active')); |
| window.addEventListener('click', (e) => e.target === addHabitModal && addHabitModal.classList.remove('active')); |
| </script> |
| </body> |
| </html> |