Done
Skip
Miss
`;
habitList.appendChild(card);
});
}
function renderCalendar() {
const calendarGrid = document.getElementById('calendar-grid');
const monthYearEl = document.getElementById('calendar-month-year');
const legendEl = document.getElementById('calendar-legend');
if (!calendarGrid || !monthYearEl || !legendEl) return;
calendarGrid.innerHTML = '';
const month = calendarDate.getMonth();
const year = calendarDate.getFullYear();
monthYearEl.textContent = `${calendarDate.toLocaleString('default', { month: 'long' })} ${year}`;
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
daysOfWeek.forEach(day => {
calendarGrid.innerHTML += `
${day}
`;
});
const firstDayOfMonth = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
for (let i = 0; i < firstDayOfMonth; i++) calendarGrid.innerHTML += '
';
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dateString = date.toISOString().split('T')[0];
let indicators = '
';
habits.forEach((habit, index) => {
const status = habit.data[dateString];
if (status) {
const color = habitColors[index % habitColors.length];
let statusClass = '';
if (status === 'completed') statusClass = `w-2 h-2 rounded-full`;
else if (status === 'skipped') statusClass = `w-2 h-2 border border-current`;
else if (status === 'missed') statusClass = `w-2 h-2`;
indicators += `
`;
}
});
indicators += '
';
const isToday = dateString === getTodayString();
const todayClass = isToday ? 'bg-blue-500 text-white font-bold' : 'bg-gray-100';
calendarGrid.innerHTML += `
${day}
${indicators}
`;
}
legendEl.innerHTML = '';
habits.forEach((habit, index) => {
const color = habitColors[index % habitColors.length];
legendEl.innerHTML += `
`;
});
}
function renderManageHabits() {
const manageList = document.getElementById('manage-habit-list');
if (!manageList) return;
manageList.innerHTML = '';
if (habits.length === 0) {
manageList.innerHTML = `
No habits added yet. `;
return;
}
habits.forEach(habit => {
const listItem = document.createElement('li');
listItem.className = 'bg-white p-3 rounded-lg border border-gray-200 flex items-center justify-between';
listItem.innerHTML = `
${habit.name}
Delete
`;
manageList.appendChild(listItem);
});
}
// SECTION: UI & EVENT HANDLING
// ============================
function switchTab(tabName) {
currentTab = tabName;
updateTabUI();
}
function updateTabUI() {
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.add('hidden'));
const activePane = document.getElementById(currentTab);
if(activePane) activePane.classList.remove('hidden');
document.querySelectorAll('.tab-btn').forEach(btn => {
if (btn.dataset.tab === currentTab) {
btn.classList.remove('tab-inactive');
btn.classList.add('tab-active');
} else {
btn.classList.remove('tab-active');
btn.classList.add('tab-inactive');
}
});
const prevBtn = document.getElementById('prev-tab-btn');
const nextBtn = document.getElementById('next-tab-btn');
if (!prevBtn || !nextBtn) return;
prevBtn.disabled = (currentTab === 'dashboard');
nextBtn.disabled = (currentTab === 'manage');
prevBtn.classList.toggle('opacity-50', prevBtn.disabled);
nextBtn.classList.toggle('opacity-50', nextBtn.disabled);
}
function navigateTabs(direction) {
const tabs = ['dashboard', 'calendar', 'manage'];
const currentIndex = tabs.indexOf(currentTab);
let newIndex = currentIndex + direction;
if (newIndex >= 0 && newIndex < tabs.length) {
switchTab(tabs[newIndex]);
}
}
function setupEventListeners() {
const newHabitInput = document.getElementById('new-habit-input');
const addHabitBtn = document.getElementById('add-habit-btn');
if (newHabitInput && addHabitBtn) {
addHabitBtn.addEventListener('click', () => {
addHabit(newHabitInput.value);
newHabitInput.value = '';
});
newHabitInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addHabit(newHabitInput.value);
newHabitInput.value = '';
}
});
}
document.getElementById('tab-content').addEventListener('click', (e) => {
const target = e.target.closest('button');
if (!target) return;
if (target.classList.contains('status-btn')) {
const habitId = parseInt(target.dataset.habitId);
const status = target.dataset.status;
setHabitStatus(habitId, getTodayString(), status);
}
if (target.classList.contains('delete-habit-btn')) {
const habitId = parseInt(target.dataset.habitId);
if (confirm('Are you sure you want to delete this habit and all its data?')) {
deleteHabit(habitId);
}
}
});
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
document.getElementById('prev-tab-btn')?.addEventListener('click', () => navigateTabs(-1));
document.getElementById('next-tab-btn')?.addEventListener('click', () => navigateTabs(1));
document.getElementById('prev-month-btn')?.addEventListener('click', () => {
calendarDate.setMonth(calendarDate.getMonth() - 1);
renderCalendar();
});
document.getElementById('next-month-btn')?.addEventListener('click', () => {
calendarDate.setMonth(calendarDate.getMonth() + 1);
renderCalendar();
});
document.getElementById('download-pdf-btn')?.addEventListener('click', generateProductivityReport);
}
// SECTION: ADVANCED PDF REPORT GENERATION
// =======================================
function generateProductivityReport() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
let y = 20; // Vertical position cursor
const pageW = doc.internal.pageSize.getWidth();
const margin = 15;
// === 1. DOCUMENT HEADER ===
doc.setFont('helvetica', 'bold');
doc.setFontSize(24);
doc.setTextColor('#1f2937'); // gray-800
doc.text('Productivity Report', pageW / 2, y, { align: 'center' });
y += 8;
doc.setFont('helvetica', 'normal');
doc.setFontSize(11);
doc.setTextColor('#6b7280'); // gray-500
const reportDate = new Date(getTodayString()).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
doc.text(`Report Generated on: ${reportDate}`, pageW / 2, y, { align: 'center' });
y += 15;
// === 2. OVERALL STATS SUMMARY ===
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Monthly Summary', margin, y);
y += 8;
const month = new Date(getTodayString()).getMonth();
const year = new Date(getTodayString()).getFullYear();
const daysInMonth = new Date(year, month + 1, 0).getDate();
let totalCompleted = 0;
let totalTrackedDays = 0;
habits.forEach(habit => {
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = new Date(year, month, day).toISOString().split('T')[0];
if (habit.data[dateStr]) {
totalTrackedDays++;
if (habit.data[dateStr] === 'completed') {
totalCompleted++;
}
}
}
});
const overallCompletion = totalTrackedDays > 0 ? ((totalCompleted / totalTrackedDays) * 100).toFixed(0) : 0;
doc.setFillColor('#f3f4f6'); // gray-100
doc.roundedRect(margin, y, pageW - (margin * 2), 25, 3, 3, 'F');
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.setTextColor('#1f2937');
doc.text('Overall Completion:', margin + 10, y + 15);
doc.text(`${overallCompletion}%`, pageW - margin - 10, y + 15, { align: 'right' });
y += 35;
// === 3. INDIVIDUAL HABIT BREAKDOWN ===
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Habit Breakdown', margin, y);
y += 5;
habits.forEach((habit, index) => {
if (y > 260) { // Add new page if content overflows
doc.addPage();
y = 20;
}
doc.setDrawColor('#e5e7eb'); // gray-200
doc.line(margin, y, pageW - margin, y); // Separator line
y += 10;
const color = habitColors[index % habitColors.length];
doc.setFillColor(color);
doc.roundedRect(margin, y, 3, 18, 1.5, 1.5, 'F');
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.setTextColor('#1f2937');
doc.text(habit.name, margin + 8, y + 5);
const streak = calculateStreak(habit.id);
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.setTextColor('#6b7280');
doc.text(`Current Streak: ${streak} day(s)`, pageW - margin, y + 5, { align: 'right' });
y += 15;
// --- Visualization: Last 30 days activity chart ---
doc.setFontSize(8);
const today = new Date(getTodayString());
const squareSize = 5;
const spacing = 1;
let startX = margin + 5;
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const status = habit.data[dateStr];
let fillColor = '#e5e7eb'; // Default gray for no data
if (status === 'completed') fillColor = color;
if (status === 'skipped') fillColor = '#fcd34d'; // amber-300
if (status === 'missed') fillColor = '#fca5a5'; // red-300
doc.setFillColor(fillColor);
doc.rect(startX, y, squareSize, squareSize, 'F');
startX += squareSize + spacing;
}
y += 15;
});
// === 4. FOOTER ===
doc.setDrawColor('#e5e7eb');
doc.line(margin, 280, pageW - margin, 280);
doc.setFontSize(9);
doc.setTextColor('#9ca3af'); // gray-400
doc.text('Generated by Habit Formation Tracker', pageW / 2, 285, { align: 'center' });
// Save the PDF
doc.save(`Productivity_Report_${getTodayString()}.pdf`);
}
// Initial Load
loadHabits();
setupEventListeners();
});