feat: US-003 - Frontend: Check/uncheck toggle behavior
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user