From 9e6ea78394e3a2daeee63acb6f7189a79ba3c11f Mon Sep 17 00:00:00 2001 From: KaioAlvesDEV <157232955+KaioAlvesDEV@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:48:57 -0300 Subject: [PATCH] Add general progress tab for daily habit tracking Introduces a 'General' tab that displays overall daily progress across all habits, including a visual calendar with color interpolation based on completion percentage. Prevents renaming or deleting the general tab, ensures it is always present and sorted at the top, and updates UI and logic in App.js, CalendarRenderer.js, HabitManager.js, and styles.css to support this feature. Yeah, I used AI, I don't know enough, but if you like it, you can rework the idea in another way. --- css/styles.css | 21 +++++++++++ js/App.js | 5 +-- js/CalendarRenderer.js | 79 +++++++++++++++++++++++++++--------------- js/HabitManager.js | 66 +++++++++++++++++++++++++++-------- 4 files changed, 127 insertions(+), 44 deletions(-) diff --git a/css/styles.css b/css/styles.css index 6b547fc..2be61af 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,3 +1,24 @@ +.general-habit { + border: 2px solid var(--day-completed); + background: linear-gradient(90deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); + box-shadow: 0 0 8px 0 var(--day-completed)33; + margin-bottom: 32px; +} +.general-habit .habit-header { + background: none; + border-bottom: 1px solid var(--day-completed); + margin-bottom: 12px; +} +.general-habit .habit-name { + font-weight: bold; + color: var(--day-completed); + font-size: 22px; +} +.general-habit .habit-stats span { + color: var(--day-completed); + font-size: 16px; + font-weight: 500; +} :root { --bg-primary: #f5f5f5; --bg-secondary: white; diff --git a/js/App.js b/js/App.js index 6929b2c..7598c25 100644 --- a/js/App.js +++ b/js/App.js @@ -47,17 +47,17 @@ class App { } deleteHabit(id) { + if (id === -1) return; // Não pode excluir a aba geral if (this.habitManager.deleteHabit(id)) { this.renderHabits(); } } renameHabit(id) { + if (id === -1) return; // Não pode renomear a aba geral const habit = this.habitManager.getHabits().find(h => h.id === id); if (!habit) return; - const newName = prompt(`Rename habit "${habit.name}" to:`, habit.name); - if (newName && newName.trim() && newName.trim() !== habit.name) { if (this.habitManager.renameHabit(id, newName)) { this.renderHabits(); @@ -118,6 +118,7 @@ class App { } openHabitSettings(habitId) { + if (habitId === -1) return; // Não pode abrir modal para aba geral this.createSettingsModal(habitId); } diff --git a/js/CalendarRenderer.js b/js/CalendarRenderer.js index da11596..93ccfb4 100644 --- a/js/CalendarRenderer.js +++ b/js/CalendarRenderer.js @@ -22,18 +22,14 @@ class CalendarRenderer { return `${year}-${month}-${day}`; } - generateCalendar(habit) { + generateCalendar(habit, habitManager) { const today = new Date(); const startDate = new Date(this.currentViewYear, 0, 1); const endDate = new Date(this.currentViewYear, 11, 31); - const firstDay = startDate.getDay(); const calendarStart = new Date(startDate); calendarStart.setDate(startDate.getDate() - firstDay); - - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; let calendar = '
'; - calendar += `
@@ -41,52 +37,80 @@ class CalendarRenderer {
`; - calendar += '
'; - for (let i = 0; i < 371; i++) { const currentDate = new Date(calendarStart); currentDate.setDate(calendarStart.getDate() + i); - if (currentDate.getFullYear() > this.currentViewYear) break; - const dateString = this.formatDateEuropean(currentDate); - const isCompleted = habit.completedDates.includes(dateString); const isToday = dateString === this.formatDateEuropean(today) && this.currentViewYear === today.getFullYear(); const isCurrentYear = currentDate.getFullYear() === this.currentViewYear; - - const dayOfWeek = currentDate.getDay(); - const weekIndex = Math.floor(i / 7); - + let extraStyle = !isCurrentYear ? 'opacity: 0.3;' : ''; + let extraClass = ''; + let onClick = ''; + let bgStyle = ''; + if (habit.isGeneral) { + const progress = habitManager.getGeneralProgress(dateString); + const root = document.documentElement; + const getVar = v => getComputedStyle(root).getPropertyValue(v).trim(); + const from = getVar('--day-default') || '#ebedf0'; + const to = getVar('--day-completed') || '#39d353'; + // Convert hex to rgb + function hexToRgb(hex) { + hex = hex.replace('#', ''); + if (hex.length === 3) hex = hex.split('').map(x => x + x).join(''); + const num = parseInt(hex, 16); + return [num >> 16, (num >> 8) & 255, num & 255]; + } + const rgbFrom = hexToRgb(from); + const rgbTo = hexToRgb(to); + // Interpolate + const rgb = rgbFrom.map((c, i) => Math.round(c + (rgbTo[i] - c) * progress)); + bgStyle = `background: rgb(${rgb[0]},${rgb[1]},${rgb[2]});`; + extraClass = progress === 1 ? 'completed' : ''; + } else { + const isCompleted = habit.completedDates.includes(dateString); + extraClass = isCompleted ? 'completed' : ''; + onClick = `onclick=\"app.toggleHabitDate(${habit.id}, '${dateString}')\"`; + } calendar += ` -
+
`; } - calendar += '
'; return calendar; } renderHabits(habitManager) { const habitsList = document.getElementById('habitsList'); - - if (habitManager.getHabits().length === 0) { + if (habitManager.getHabits().length === 0 || habitManager.getHabits().filter(h => !h.isGeneral).length === 0) { habitsList.innerHTML = '

No habits yet. Add one above!

'; return; } - const sortedHabits = habitManager.getSortedHabits(); - habitsList.innerHTML = sortedHabits.map(habit => { const currentYear = new Date().getFullYear(); const yearTotal = habitManager.getYearStats(habit, this.currentViewYear); const isCurrentYear = this.currentViewYear === currentYear; - + if (habit.isGeneral) { + // General tab highlighted + return ` +
+
+
${habit.name} (Daily Progress)
+
+ Today's progress: ${(habitManager.getGeneralProgress(this.formatDateEuropean(new Date()))*100).toFixed(0)}% +
+
+ ${this.generateCalendar(habit, habitManager)} +
+ `; + } return `
@@ -101,9 +125,10 @@ class CalendarRenderer {
${this.generateMotivationalMessageArea(habit)} - ${this.generateCalendar(habit)} + ${this.generateCalendar(habit, habitManager)}
- `}).join(''); + `; + }).join(''); } generateMotivationalMessageArea(habit) { diff --git a/js/HabitManager.js b/js/HabitManager.js index c297401..06d30d5 100644 --- a/js/HabitManager.js +++ b/js/HabitManager.js @@ -2,6 +2,23 @@ class HabitManager { constructor() { this.habits = this.loadHabits(); this.currentSortBy = localStorage.getItem('sortBy') || 'name'; + this.ensureGeneralTab(); + } + // Garante que a aba geral existe e está sempre na posição 0 + ensureGeneralTab() { + if (!this.habits.length || this.habits[0]?.isGeneral !== true) { + const generalHabit = { + id: -1, + name: 'General', + isGeneral: true, + completedDates: [], + streak: 0, + motivationalMessages: [], + currentDisplayMessage: null + }; + this.habits.unshift(generalHabit); + this.saveHabits(); + } } loadHabits() { @@ -24,7 +41,6 @@ class HabitManager { addHabit(name) { if (!name || !name.trim()) return false; - const habit = { id: Date.now(), name: name.trim(), @@ -32,33 +48,42 @@ class HabitManager { streak: 0, motivationalMessages: [] }; - this.habits.push(habit); this.saveHabits(); + this.ensureGeneralTab(); return true; } deleteHabit(id) { + if (id === -1) return false; // Não pode excluir a aba geral const habit = this.habits.find(h => h.id === id); if (!habit) return false; - const confirmed = confirm(`Are you sure you want to delete the habit "${habit.name}"? This will permanently remove all ${habit.completedDates.length} completed days.`); if (confirmed) { this.habits = this.habits.filter(h => h.id !== id); this.saveHabits(); + this.ensureGeneralTab(); return true; } return false; } renameHabit(id, newName) { + if (id === -1) return false; // Não pode renomear a aba geral const habit = this.habits.find(h => h.id === id); if (!habit || !newName || !newName.trim()) return false; - habit.name = newName.trim(); this.saveHabits(); return true; } + // Calcula a proporção de hábitos marcados para um dia + getGeneralProgress(dateString) { + // Não conta a aba geral + const habits = this.habits.filter(h => !h.isGeneral); + if (habits.length === 0) return 0; + const completed = habits.filter(h => h.completedDates.includes(dateString)).length; + return completed / habits.length; + } toggleHabitDate(habitId, dateString) { const habit = this.habits.find(h => h.id === habitId); @@ -115,28 +140,39 @@ class HabitManager { } getSortedHabits() { - const sortedHabits = [...this.habits]; - + // A aba geral sempre fica no topo + const general = this.habits.find(h => h.isGeneral); + const others = this.habits.filter(h => !h.isGeneral); + let sortedHabits; switch (this.currentSortBy) { case 'name': - return sortedHabits.sort((a, b) => a.name.localeCompare(b.name)); + sortedHabits = others.sort((a, b) => a.name.localeCompare(b.name)); + break; case 'name-desc': - return sortedHabits.sort((a, b) => b.name.localeCompare(a.name)); + sortedHabits = others.sort((a, b) => b.name.localeCompare(a.name)); + break; case 'streak': - return sortedHabits.sort((a, b) => b.streak - a.streak); + sortedHabits = others.sort((a, b) => b.streak - a.streak); + break; case 'streak-asc': - return sortedHabits.sort((a, b) => a.streak - b.streak); + sortedHabits = others.sort((a, b) => a.streak - b.streak); + break; case 'total': - return sortedHabits.sort((a, b) => b.completedDates.length - a.completedDates.length); + sortedHabits = others.sort((a, b) => b.completedDates.length - a.completedDates.length); + break; case 'total-asc': - return sortedHabits.sort((a, b) => a.completedDates.length - b.completedDates.length); + sortedHabits = others.sort((a, b) => a.completedDates.length - b.completedDates.length); + break; case 'recent': - return sortedHabits.sort((a, b) => b.id - a.id); + sortedHabits = others.sort((a, b) => b.id - a.id); + break; case 'oldest': - return sortedHabits.sort((a, b) => a.id - b.id); + sortedHabits = others.sort((a, b) => a.id - b.id); + break; default: - return sortedHabits; + sortedHabits = others; } + return general ? [general, ...sortedHabits] : sortedHabits; } clearAllData() {