Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 3 additions & 2 deletions js/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -118,6 +118,7 @@ class App {
}

openHabitSettings(habitId) {
if (habitId === -1) return; // Não pode abrir modal para aba geral
this.createSettingsModal(habitId);
}

Expand Down
79 changes: 52 additions & 27 deletions js/CalendarRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,71 +22,95 @@ 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 = '<div class="calendar">';

calendar += `
<div class="year-controls">
<button class="year-btn" onclick="app.changeYear(-1)">←</button>
<div class="current-year">${this.currentViewYear}</div>
<button class="year-btn" onclick="app.changeYear(1)" ${this.currentViewYear >= today.getFullYear() ? 'disabled' : ''}>→</button>
</div>
`;

calendar += '<div class="calendar-grid">';

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 += `
<div class="day-square ${isCompleted ? 'completed' : ''} ${isToday ? 'today' : ''} ${!isCurrentYear ? 'other-year' : ''}"
onclick="app.toggleHabitDate(${habit.id}, '${dateString}')"
onmouseenter="app.showTooltip(event, '${dateString}')"
onmouseleave="app.hideTooltip()"
style="grid-column: ${weekIndex + 1}; grid-row: ${dayOfWeek + 1}; ${!isCurrentYear ? 'opacity: 0.3;' : ''}">
<div class=\"day-square ${extraClass} ${isToday ? 'today' : ''} ${!isCurrentYear ? 'other-year' : ''}\"
${onClick}
onmouseenter=\"app.showTooltip(event, '${dateString}')\"
onmouseleave=\"app.hideTooltip()\"
style=\"grid-column: ${Math.floor(i / 7) + 1}; grid-row: ${currentDate.getDay() + 1}; ${extraStyle} ${bgStyle}\">
</div>
`;
}

calendar += '</div></div>';
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 = '<p style="text-align: center; color: #666;">No habits yet. Add one above!</p>';
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 `
<div class="habit general-habit">
<div class="habit-header">
<div class="habit-name">${habit.name} <span style='font-size:14px;color:var(--day-completed);'>(Daily Progress)</span></div>
<div class="habit-stats">
<span>Today's progress: ${(habitManager.getGeneralProgress(this.formatDateEuropean(new Date()))*100).toFixed(0)}%</span>
</div>
</div>
${this.generateCalendar(habit, habitManager)}
</div>
`;
}
return `
<div class="habit">
<div class="habit-header">
Expand All @@ -101,9 +125,10 @@ class CalendarRenderer {
</div>
</div>
${this.generateMotivationalMessageArea(habit)}
${this.generateCalendar(habit)}
${this.generateCalendar(habit, habitManager)}
</div>
`}).join('');
`;
}).join('');
}

generateMotivationalMessageArea(habit) {
Expand Down
66 changes: 51 additions & 15 deletions js/HabitManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -24,41 +41,49 @@ class HabitManager {

addHabit(name) {
if (!name || !name.trim()) return false;

const habit = {
id: Date.now(),
name: name.trim(),
completedDates: [],
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);
Expand Down Expand Up @@ -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() {
Expand Down