|
<!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> |