Files
llm-bot-framework/templates/index.html
2026-04-29 17:41:10 -05:00

479 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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