feat: US-003 - Frontend: Check/uncheck toggle behavior

This commit is contained in:
Echo
2026-02-10 18:50:26 +00:00
parent 081121e48d
commit 9d9f00e069
2 changed files with 236 additions and 44 deletions

View File

@@ -302,20 +302,23 @@
flex-shrink: 0;
}
.habit-card-check-btn-compact:hover:not(:disabled) {
.habit-card-check-btn-compact:hover {
background: var(--accent);
color: white;
transform: scale(1.1);
}
.habit-card-check-btn-compact:disabled {
opacity: 0.6;
cursor: not-allowed;
.habit-card-check-btn-compact.checked {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.habit-card-check-btn-compact.checked:hover {
opacity: 0.8;
transform: scale(1.05);
}
.habit-card-actions {
display: flex;
gap: var(--space-1);
@@ -1296,12 +1299,12 @@
container.innerHTML = `<div class="habits-grid">${habitsHtml}</div>`;
lucide.createIcons();
// Attach event handlers to check-in buttons
// Attach event handlers to ALL check-in buttons (for toggle behavior)
habits.forEach(habit => {
if (!isCheckedToday(habit)) {
const btn = document.getElementById(`checkin-btn-${habit.id}`);
if (btn) {
// Right-click to open detail modal
const btn = document.getElementById(`checkin-btn-${habit.id}`);
if (btn) {
// Right-click to open detail modal (only for unchecked habits)
if (!isCheckedToday(habit)) {
btn.addEventListener('contextmenu', (e) => handleCheckInButtonPress(habit.id, e, true));
// Mouse/touch events for long-press detection
@@ -1312,6 +1315,9 @@
btn.addEventListener('touchstart', (e) => handleCheckInButtonPress(habit.id, e, false));
btn.addEventListener('touchend', (e) => handleCheckInButtonRelease(habit.id, e));
btn.addEventListener('touchcancel', () => handleCheckInButtonCancel());
} else {
// For checked habits, simple click to uncheck
btn.addEventListener('click', (e) => checkInHabit(habit.id, e));
}
}
});
@@ -1330,10 +1336,9 @@
<span class="habit-card-name">${escapeHtml(habit.name)}</span>
<span class="habit-card-streak">🔥 ${habit.streak?.current || 0}</span>
<button
class="habit-card-check-btn-compact"
class="habit-card-check-btn-compact ${isDoneToday ? 'checked' : ''}"
id="checkin-btn-${habit.id}"
${isDoneToday ? 'disabled' : ''}
title="${isDoneToday ? 'Completed today' : 'Check in'}"
title="${isDoneToday ? 'Click to uncheck' : 'Check in'}"
>
${isDoneToday ? '✓' : '○'}
</button>
@@ -1967,21 +1972,63 @@
let checkInMood = null;
let longPressTimer = null;
// Check in habit (simple click)
// Check in or uncheck habit (toggle)
async function checkInHabit(habitId, event) {
// Prevent simple check-in if this was triggered during long-press detection
if (event && event.type === 'mousedown') {
return; // Let the long-press handler deal with it
}
// Find the habit to check current state
const habit = habits.find(h => h.id === habitId);
if (!habit) {
showToast('Habit not found', 'error');
return;
}
const isChecked = isCheckedToday(habit);
const today = new Date().toISOString().split('T')[0];
// Get the check button for optimistic UI update
const btn = document.getElementById(`checkin-btn-${habitId}`);
const card = event?.target?.closest('.habit-card');
const streakElement = card?.querySelector('.habit-card-streak');
// Store original state for rollback on error
const originalButtonText = btn?.innerHTML;
const originalButtonDisabled = btn?.disabled;
const originalStreakText = streakElement?.textContent;
try {
const response = await fetch(`/echo/api/habits/${habitId}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
// Optimistic UI update
if (btn) {
if (isChecked) {
// Unchecking - show unchecked state
btn.innerHTML = '○';
btn.disabled = false;
} else {
// Checking - show checked state
btn.innerHTML = '✓';
btn.disabled = true;
}
}
let response;
if (isChecked) {
// Send DELETE request to uncheck
response = await fetch(`/echo/api/habits/${habitId}/check?date=${today}`, {
method: 'DELETE'
});
} else {
// Send POST request to check
response = await fetch(`/echo/api/habits/${habitId}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
}
if (!response.ok) {
const error = await response.json();
@@ -1990,24 +2037,40 @@
const updatedHabit = await response.json();
// Show success toast with streak
showToast(`Habit checked! 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
// Update streak display immediately
if (streakElement) {
streakElement.textContent = `🔥 ${updatedHabit.streak?.current || 0}`;
}
// Refresh habits list
// Show success toast with appropriate message
if (isChecked) {
showToast(`Check-in removed. 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
} else {
showToast(`Habit checked! 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
}
// Refresh habits list to get all updated data
await loadHabits();
// Add pulse animation to the updated card
setTimeout(() => {
const card = event?.target?.closest('.habit-card');
if (card) {
card.classList.add('pulse');
setTimeout(() => card.classList.remove('pulse'), 500);
}
}, 100);
if (card) {
card.classList.add('pulse');
setTimeout(() => card.classList.remove('pulse'), 500);
}
} catch (error) {
console.error('Failed to check in:', error);
showToast('Failed to check in: ' + error.message, 'error');
console.error('Failed to toggle check-in:', error);
// Revert optimistic UI update on error
if (btn) {
btn.innerHTML = originalButtonText;
btn.disabled = originalButtonDisabled;
}
if (streakElement) {
streakElement.textContent = originalStreakText;
}
showToast('Failed to toggle check-in: ' + error.message, 'error');
}
}