first commit
This commit is contained in:
0
templates/New Text Document.txt
Normal file
0
templates/New Text Document.txt
Normal file
479
templates/index.html
Normal file
479
templates/index.html
Normal file
@@ -0,0 +1,479 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Habit Tracker</title>
|
||||
<link rel="stylesheet" href="../static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo-icon">✅</div>
|
||||
<div class="header-title">
|
||||
<h1>Habit Tracker</h1>
|
||||
<span class="date-display" id="dateDisplay"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn-icon" id="themeToggle" title="Toggle dark mode" aria-label="Toggle dark mode">🌓</button>
|
||||
<button class="btn btn-primary" id="showAddForm">
|
||||
<span>+</span> New Habit
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card accent">
|
||||
<div class="stat-icon">📋</div>
|
||||
<div class="stat-value" id="totalHabits">0</div>
|
||||
<div class="stat-label">Total Habits</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-icon">🎯</div>
|
||||
<div class="stat-value" id="completedToday">0</div>
|
||||
<div class="stat-label">Done Today</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-icon">🔥</div>
|
||||
<div class="stat-value" id="bestStreak">0</div>
|
||||
<div class="stat-label">Best Streak</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Habit List -->
|
||||
<div class="habit-list-title">Your Habits</div>
|
||||
<div class="habit-list" id="habitList">
|
||||
<!-- Empty state (shown when no habits exist) -->
|
||||
<div class="empty-state" id="emptyState">
|
||||
<div class="empty-icon">🌱</div>
|
||||
<p>No habits yet</p>
|
||||
<p class="sub">Click "New Habit" to get started!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Habit Form -->
|
||||
<div class="add-habit-section" id="addHabitSection">
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex: 2; min-width: 160px;">
|
||||
<label for="habitName">Habit Name</label>
|
||||
<input type="text" id="habitName" placeholder="e.g., Read for 30 min" maxlength="40">
|
||||
</div>
|
||||
<div class="form-group" style="flex: 1; min-width: 100px;">
|
||||
<label for="habitCategory">Category</label>
|
||||
<select id="habitCategory">
|
||||
<option value="health">💪 Health</option>
|
||||
<option value="mind">🧠 Mind</option>
|
||||
<option value="productivity">⚡ Productivity</option>
|
||||
<option value="wellness">🧘 Wellness</option>
|
||||
<option value="other">📌 Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex: 0; min-width: auto; align-self: flex-end;">
|
||||
<button class="btn btn-primary" id="addHabitBtn">Add</button>
|
||||
</div>
|
||||
<div class="form-group" style="flex: 0; min-width: auto; align-self: flex-end;">
|
||||
<button class="btn btn-outline btn-sm" id="cancelAddBtn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer-actions">
|
||||
<button class="btn btn-outline btn-sm" id="resetTodayBtn">🔄 Reset Today</button>
|
||||
<button class="btn btn-outline btn-sm" id="resetAllBtn">🗑️ Clear All Habits</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// --- State ---
|
||||
const STORAGE_KEY = 'habitTrackerData_v2';
|
||||
const THEME_KEY = 'habitTrackerTheme';
|
||||
|
||||
let habits = [];
|
||||
let theme = localStorage.getItem(THEME_KEY) || 'light';
|
||||
|
||||
// --- DOM refs ---
|
||||
const habitListEl = document.getElementById('habitList');
|
||||
const emptyStateEl = document.getElementById('emptyState');
|
||||
const totalHabitsEl = document.getElementById('totalHabits');
|
||||
const completedTodayEl = document.getElementById('completedToday');
|
||||
const bestStreakEl = document.getElementById('bestStreak');
|
||||
const dateDisplayEl = document.getElementById('dateDisplay');
|
||||
const addHabitSection = document.getElementById('addHabitSection');
|
||||
const habitNameInput = document.getElementById('habitName');
|
||||
const habitCategorySelect = document.getElementById('habitCategory');
|
||||
const themeToggleBtn = document.getElementById('themeToggle');
|
||||
const toastEl = document.getElementById('toast');
|
||||
|
||||
// --- Load data ---
|
||||
function loadHabits() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
habits = JSON.parse(raw);
|
||||
} else {
|
||||
// Default demo habits
|
||||
habits = [
|
||||
{ id: genId(), name: 'Read for 30 minutes', category: 'mind', streak: 5,
|
||||
completedDates: getRecentDates(5), colorIndex: 0 },
|
||||
{ id: genId(), name: 'Drink 8 glasses of water', category: 'health', streak: 12,
|
||||
completedDates: getRecentDates(12), colorIndex: 1 },
|
||||
{ id: genId(), name: 'Meditate (10 min)', category: 'wellness', streak: 3,
|
||||
completedDates: getRecentDates(3), colorIndex: 2 },
|
||||
{ id: genId(), name: 'No social media before bed', category: 'productivity', streak: 0,
|
||||
completedDates: [], colorIndex: 3 },
|
||||
];
|
||||
saveHabits();
|
||||
}
|
||||
} catch (e) {
|
||||
habits = [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveHabits() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(habits));
|
||||
}
|
||||
|
||||
function genId() {
|
||||
return 'h_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
|
||||
}
|
||||
|
||||
function getRecentDates(n) {
|
||||
const dates = [];
|
||||
const today = new Date();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
dates.push(formatDate(d));
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function getTodayStr() {
|
||||
return formatDate(new Date());
|
||||
}
|
||||
|
||||
// --- Theme ---
|
||||
function applyTheme() {
|
||||
if (theme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
themeToggleBtn.textContent = '☀️';
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
themeToggleBtn.textContent = '🌙';
|
||||
}
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme = theme === 'light' ? 'dark' : 'light';
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
// --- Toast ---
|
||||
let toastTimeout;
|
||||
|
||||
function showToast(msg) {
|
||||
clearTimeout(toastTimeout);
|
||||
toastEl.textContent = msg;
|
||||
toastEl.classList.add('show');
|
||||
toastTimeout = setTimeout(() => {
|
||||
toastEl.classList.remove('show');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
function renderAll() {
|
||||
renderStats();
|
||||
renderHabitList();
|
||||
renderDate();
|
||||
}
|
||||
|
||||
function renderDate() {
|
||||
const now = new Date();
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
dateDisplayEl.textContent = now.toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const todayStr = getTodayStr();
|
||||
totalHabitsEl.textContent = habits.length;
|
||||
const completedToday = habits.filter(h => h.completedDates.includes(todayStr)).length;
|
||||
completedTodayEl.textContent = completedToday;
|
||||
const best = habits.reduce((max, h) => Math.max(max, h.streak || 0), 0);
|
||||
bestStreakEl.textContent = best;
|
||||
}
|
||||
|
||||
function renderHabitList() {
|
||||
// Clear list (keep empty state element)
|
||||
habitListEl.querySelectorAll('.habit-item').forEach(el => el.remove());
|
||||
|
||||
if (habits.length === 0) {
|
||||
emptyStateEl.style.display = '';
|
||||
} else {
|
||||
emptyStateEl.style.display = 'none';
|
||||
const todayStr = getTodayStr();
|
||||
habits.forEach((habit, index) => {
|
||||
const isCompleted = habit.completedDates.includes(todayStr);
|
||||
const el = createHabitElement(habit, index, isCompleted);
|
||||
habitListEl.appendChild(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createHabitElement(habit, index, isCompleted) {
|
||||
const colorPalette = [
|
||||
'#6c5ce7', '#00b894', '#e17055', '#0984e3',
|
||||
'#fdcb6e', '#e84393', '#00cec9', '#6ab04c'
|
||||
];
|
||||
const color = colorPalette[habit.colorIndex % colorPalette.length] || colorPalette[0];
|
||||
const categoryEmojis = {
|
||||
health: '💪',
|
||||
mind: '🧠',
|
||||
productivity: '⚡',
|
||||
wellness: '🧘',
|
||||
other: '📌'
|
||||
};
|
||||
const categoryEmoji = categoryEmojis[habit.category] || '📌';
|
||||
const weeklyGoal = 7;
|
||||
const thisWeekCompletions = getThisWeekCompletions(habit);
|
||||
const weeklyProgress = Math.min(100, Math.round((thisWeekCompletions / weeklyGoal) * 100));
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'habit-item' + (isCompleted ? ' completed' : '');
|
||||
div.setAttribute('data-habit-id', habit.id);
|
||||
div.innerHTML = `
|
||||
<div class="habit-checkbox">✓</div>
|
||||
<div class="habit-info">
|
||||
<div class="habit-name">${escapeHtml(habit.name)}</div>
|
||||
<div class="habit-details">
|
||||
<span class="habit-streak">
|
||||
<span class="streak-flame">🔥</span> ${habit.streak || 0} day${habit.streak !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span class="habit-category" style="background: ${color}15; color: ${color};">
|
||||
${categoryEmoji} ${capitalize(habit.category)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="habit-progress-bar-wrap">
|
||||
<div class="habit-progress-bar-fill" style="width: ${weeklyProgress}%; background: ${color};"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="habit-delete" title="Delete habit" data-delete-id="${habit.id}">🗑️</button>
|
||||
`;
|
||||
|
||||
// Click to toggle
|
||||
div.addEventListener('click', (e) => {
|
||||
// Don't toggle if clicking delete button
|
||||
if (e.target.closest('.habit-delete')) return;
|
||||
toggleHabitCompletion(habit.id);
|
||||
});
|
||||
|
||||
// Delete button
|
||||
const deleteBtn = div.querySelector('.habit-delete');
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
deleteHabit(habit.id);
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
function getThisWeekCompletions(habit) {
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay(); // 0=Sun
|
||||
const startOfWeek = new Date(today);
|
||||
startOfWeek.setDate(today.getDate() - ((dayOfWeek + 6) % 7)); // Monday start
|
||||
let count = 0;
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(startOfWeek);
|
||||
d.setDate(startOfWeek.getDate() + i);
|
||||
const dStr = formatDate(d);
|
||||
if (habit.completedDates.includes(dStr)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
function toggleHabitCompletion(habitId) {
|
||||
const habit = habits.find(h => h.id === habitId);
|
||||
if (!habit) return;
|
||||
const todayStr = getTodayStr();
|
||||
const idx = habit.completedDates.indexOf(todayStr);
|
||||
|
||||
if (idx >= 0) {
|
||||
// Uncomplete
|
||||
habit.completedDates.splice(idx, 1);
|
||||
recalculateStreak(habit);
|
||||
showToast('Marked as incomplete');
|
||||
} else {
|
||||
// Complete
|
||||
habit.completedDates.push(todayStr);
|
||||
recalculateStreak(habit);
|
||||
showToast('✅ Habit completed!');
|
||||
}
|
||||
saveHabits();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function recalculateStreak(habit) {
|
||||
// Sort dates descending
|
||||
const sorted = [...new Set(habit.completedDates)].sort().reverse();
|
||||
if (sorted.length === 0) {
|
||||
habit.streak = 0;
|
||||
return;
|
||||
}
|
||||
const todayStr = getTodayStr();
|
||||
const yesterdayStr = formatDate(new Date(Date.now() - 86400000));
|
||||
|
||||
// Streak must include today or yesterday to be active
|
||||
if (sorted[0] !== todayStr && sorted[0] !== yesterdayStr) {
|
||||
habit.streak = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let streak = 0;
|
||||
let checkDate = new Date(sorted[0] + 'T00:00:00');
|
||||
for (const dateStr of sorted) {
|
||||
const d = new Date(dateStr + 'T00:00:00');
|
||||
const diffDays = Math.round((checkDate - d) / 86400000);
|
||||
if (diffDays === 0) {
|
||||
streak++;
|
||||
checkDate = new Date(d.getTime() - 86400000);
|
||||
} else if (diffDays === 1 && streak === 0) {
|
||||
// Allow starting from yesterday
|
||||
streak++;
|
||||
checkDate = new Date(d.getTime() - 86400000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
habit.streak = streak;
|
||||
}
|
||||
|
||||
function addHabit() {
|
||||
const name = habitNameInput.value.trim();
|
||||
if (!name) {
|
||||
showToast('Please enter a habit name');
|
||||
habitNameInput.focus();
|
||||
return;
|
||||
}
|
||||
const category = habitCategorySelect.value;
|
||||
const newHabit = {
|
||||
id: genId(),
|
||||
name,
|
||||
category,
|
||||
streak: 0,
|
||||
completedDates: [],
|
||||
colorIndex: habits.length,
|
||||
};
|
||||
habits.push(newHabit);
|
||||
saveHabits();
|
||||
renderAll();
|
||||
habitNameInput.value = '';
|
||||
addHabitSection.classList.remove('visible');
|
||||
showToast('🎉 Habit added!');
|
||||
}
|
||||
|
||||
function deleteHabit(habitId) {
|
||||
habits = habits.filter(h => h.id !== habitId);
|
||||
saveHabits();
|
||||
renderAll();
|
||||
showToast('Habit deleted');
|
||||
}
|
||||
|
||||
function resetToday() {
|
||||
const todayStr = getTodayStr();
|
||||
let changed = false;
|
||||
habits.forEach(h => {
|
||||
const idx = h.completedDates.indexOf(todayStr);
|
||||
if (idx >= 0) {
|
||||
h.completedDates.splice(idx, 1);
|
||||
changed = true;
|
||||
}
|
||||
recalculateStreak(h);
|
||||
});
|
||||
if (changed) {
|
||||
saveHabits();
|
||||
renderAll();
|
||||
showToast('🔄 Today\'s progress reset');
|
||||
} else {
|
||||
showToast('Nothing to reset for today');
|
||||
}
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
if (confirm('Are you sure you want to delete all habits? This cannot be undone.')) {
|
||||
habits = [];
|
||||
saveHabits();
|
||||
renderAll();
|
||||
showToast('All habits cleared');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Event Listeners ---
|
||||
document.getElementById('showAddForm').addEventListener('click', () => {
|
||||
addHabitSection.classList.toggle('visible');
|
||||
if (addHabitSection.classList.contains('visible')) {
|
||||
habitNameInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('cancelAddBtn').addEventListener('click', () => {
|
||||
addHabitSection.classList.remove('visible');
|
||||
habitNameInput.value = '';
|
||||
});
|
||||
|
||||
document.getElementById('addHabitBtn').addEventListener('click', addHabit);
|
||||
habitNameInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') addHabit();
|
||||
});
|
||||
|
||||
document.getElementById('resetTodayBtn').addEventListener('click', resetToday);
|
||||
document.getElementById('resetAllBtn').addEventListener('click', resetAll);
|
||||
themeToggleBtn.addEventListener('click', toggleTheme);
|
||||
|
||||
// --- Init ---
|
||||
loadHabits();
|
||||
applyTheme();
|
||||
renderAll();
|
||||
|
||||
// Re-render stats periodically (in case date rolls over)
|
||||
setInterval(() => {
|
||||
renderStats();
|
||||
renderDate();
|
||||
}, 60000);
|
||||
|
||||
console.log('🌱 Habit Tracker ready!');
|
||||
console.log(' - Click a habit to toggle completion');
|
||||
console.log(' - Streaks auto-calculate');
|
||||
console.log(' - Data saved to localStorage');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user