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() {