first commit

This commit is contained in:
2026-04-29 17:41:10 -05:00
parent f53104d947
commit 0fe6bd7ea6
15 changed files with 1678 additions and 33 deletions

479
templates/index.html Normal file
View 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>