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 += `
-
';
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 `
+
+
+ ${this.generateCalendar(habit, habitManager)}
+
+ `;
+ }
return `
${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() {