feat: US-010 - Frontend - Check-in interaction (click and long-press)
This commit is contained in:
@@ -522,6 +522,74 @@
|
||||
.toast.error svg {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* Pulse animation */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Check-in detail modal */
|
||||
.checkin-modal {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
justify-content: center;
|
||||
margin: var(--space-3) 0;
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.rating-star.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rating-star:hover {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mood-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
justify-content: center;
|
||||
margin: var(--space-3) 0;
|
||||
}
|
||||
|
||||
.mood-btn {
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
padding: var(--space-2);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-base);
|
||||
background: none;
|
||||
}
|
||||
|
||||
.mood-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.mood-btn.selected {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -659,6 +727,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check-in Detail Modal -->
|
||||
<div id="checkinModal" class="modal-overlay">
|
||||
<div class="modal checkin-modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Check-In Details</h2>
|
||||
<button class="modal-close" onclick="closeCheckinModal()">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Note -->
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="checkinNote">Note (optional)</label>
|
||||
<textarea id="checkinNote" class="form-textarea" placeholder="How did it go?"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="form-field">
|
||||
<label class="form-label">Rating (optional)</label>
|
||||
<div class="rating-stars" id="ratingStars">
|
||||
<span class="rating-star" data-rating="1" onclick="selectRating(1)">⭐</span>
|
||||
<span class="rating-star" data-rating="2" onclick="selectRating(2)">⭐</span>
|
||||
<span class="rating-star" data-rating="3" onclick="selectRating(3)">⭐</span>
|
||||
<span class="rating-star" data-rating="4" onclick="selectRating(4)">⭐</span>
|
||||
<span class="rating-star" data-rating="5" onclick="selectRating(5)">⭐</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mood -->
|
||||
<div class="form-field">
|
||||
<label class="form-label">Mood (optional)</label>
|
||||
<div class="mood-buttons">
|
||||
<button class="mood-btn" data-mood="happy" onclick="selectMood('happy')">😊</button>
|
||||
<button class="mood-btn" data-mood="neutral" onclick="selectMood('neutral')">😐</button>
|
||||
<button class="mood-btn" data-mood="sad" onclick="selectMood('sad')">😞</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" onclick="closeCheckinModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitCheckinBtn" onclick="submitCheckInDetail()">
|
||||
Done!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Theme management
|
||||
function initTheme() {
|
||||
@@ -722,6 +837,26 @@
|
||||
const habitsHtml = habits.map(habit => renderHabitCard(habit)).join('');
|
||||
container.innerHTML = `<div class="habits-grid">${habitsHtml}</div>`;
|
||||
lucide.createIcons();
|
||||
|
||||
// Attach event handlers to check-in buttons
|
||||
habits.forEach(habit => {
|
||||
if (!isCheckedToday(habit)) {
|
||||
const btn = document.getElementById(`checkin-btn-${habit.id}`);
|
||||
if (btn) {
|
||||
// Right-click to open detail modal
|
||||
btn.addEventListener('contextmenu', (e) => handleCheckInButtonPress(habit.id, e, true));
|
||||
|
||||
// Mouse/touch events for long-press detection
|
||||
btn.addEventListener('mousedown', (e) => handleCheckInButtonPress(habit.id, e, true));
|
||||
btn.addEventListener('mouseup', (e) => handleCheckInButtonRelease(habit.id, e));
|
||||
btn.addEventListener('mouseleave', () => handleCheckInButtonCancel());
|
||||
|
||||
btn.addEventListener('touchstart', (e) => handleCheckInButtonPress(habit.id, e, false));
|
||||
btn.addEventListener('touchend', (e) => handleCheckInButtonRelease(habit.id, e));
|
||||
btn.addEventListener('touchcancel', () => handleCheckInButtonCancel());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render single habit card
|
||||
@@ -757,7 +892,7 @@
|
||||
|
||||
<button
|
||||
class="habit-card-check-btn"
|
||||
onclick="checkInHabit('${habit.id}')"
|
||||
id="checkin-btn-${habit.id}"
|
||||
${isDoneToday ? 'disabled' : ''}
|
||||
>
|
||||
${isDoneToday ? '✓ Done today' : 'Check In'}
|
||||
@@ -897,6 +1032,11 @@
|
||||
if (e.target === modal) {
|
||||
closeHabitModal();
|
||||
}
|
||||
|
||||
const checkinModal = document.getElementById('checkinModal');
|
||||
if (e.target === checkinModal) {
|
||||
closeCheckinModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize color picker
|
||||
@@ -1201,9 +1341,209 @@
|
||||
alert('Delete functionality - coming in next story!');
|
||||
}
|
||||
|
||||
// Check in habit (placeholder)
|
||||
async function checkInHabit(habitId) {
|
||||
alert('Check-in functionality - coming in next story!');
|
||||
// Check-in state
|
||||
let checkInHabitId = null;
|
||||
let checkInRating = null;
|
||||
let checkInMood = null;
|
||||
let longPressTimer = null;
|
||||
|
||||
// Check in habit (simple click)
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
const 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();
|
||||
throw new Error(error.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const updatedHabit = await response.json();
|
||||
|
||||
// Show success toast with streak
|
||||
showToast(`Habit checked! 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
|
||||
|
||||
// Refresh habits list
|
||||
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);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check in:', error);
|
||||
showToast('Failed to check in: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show check-in detail modal
|
||||
function showCheckInDetailModal(habitId) {
|
||||
checkInHabitId = habitId;
|
||||
checkInRating = null;
|
||||
checkInMood = null;
|
||||
|
||||
// Reset form
|
||||
document.getElementById('checkinNote').value = '';
|
||||
|
||||
// Reset rating stars
|
||||
document.querySelectorAll('.rating-star').forEach(star => {
|
||||
star.classList.remove('active');
|
||||
});
|
||||
|
||||
// Reset mood buttons
|
||||
document.querySelectorAll('.mood-btn').forEach(btn => {
|
||||
btn.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Show modal
|
||||
const modal = document.getElementById('checkinModal');
|
||||
modal.classList.add('active');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Close check-in detail modal
|
||||
function closeCheckinModal() {
|
||||
const modal = document.getElementById('checkinModal');
|
||||
modal.classList.remove('active');
|
||||
checkInHabitId = null;
|
||||
checkInRating = null;
|
||||
checkInMood = null;
|
||||
}
|
||||
|
||||
// Select rating
|
||||
function selectRating(rating) {
|
||||
checkInRating = rating;
|
||||
|
||||
// Update star display
|
||||
document.querySelectorAll('.rating-star').forEach((star, index) => {
|
||||
if (index < rating) {
|
||||
star.classList.add('active');
|
||||
} else {
|
||||
star.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Select mood
|
||||
function selectMood(mood) {
|
||||
checkInMood = mood;
|
||||
|
||||
// Update mood button display
|
||||
document.querySelectorAll('.mood-btn').forEach(btn => {
|
||||
if (btn.dataset.mood === mood) {
|
||||
btn.classList.add('selected');
|
||||
} else {
|
||||
btn.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Submit check-in with details
|
||||
async function submitCheckInDetail() {
|
||||
if (!checkInHabitId) {
|
||||
showToast('No habit selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = document.getElementById('submitCheckinBtn');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const note = document.getElementById('checkinNote').value.trim();
|
||||
|
||||
// Build request body with optional fields
|
||||
const body = {};
|
||||
if (note) body.note = note;
|
||||
if (checkInRating) body.rating = checkInRating;
|
||||
if (checkInMood) body.mood = checkInMood;
|
||||
|
||||
const response = await fetch(`/echo/api/habits/${checkInHabitId}/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const updatedHabit = await response.json();
|
||||
|
||||
// Show success toast with streak
|
||||
showToast(`Habit checked! 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
|
||||
|
||||
// Close modal
|
||||
closeCheckinModal();
|
||||
|
||||
// Refresh habits list
|
||||
await loadHabits();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check in:', error);
|
||||
showToast('Failed to check in: ' + error.message, 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle long-press for check-in detail modal
|
||||
function handleCheckInButtonPress(habitId, event, isMouseEvent) {
|
||||
// Prevent default context menu on right-click
|
||||
if (event.type === 'contextmenu') {
|
||||
event.preventDefault();
|
||||
showCheckInDetailModal(habitId);
|
||||
return;
|
||||
}
|
||||
|
||||
// For touch/mouse press, start long-press timer
|
||||
if (event.type === 'mousedown' || event.type === 'touchstart') {
|
||||
event.preventDefault();
|
||||
|
||||
longPressTimer = setTimeout(() => {
|
||||
showCheckInDetailModal(habitId);
|
||||
longPressTimer = null;
|
||||
}, 500); // 500ms for long-press
|
||||
}
|
||||
}
|
||||
|
||||
// Handle release of check-in button
|
||||
function handleCheckInButtonRelease(habitId, event) {
|
||||
if (event.type === 'mouseup' || event.type === 'touchend') {
|
||||
// If long-press timer is still running, it was a short press
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
|
||||
// Perform simple check-in
|
||||
checkInHabit(habitId, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel long-press if moved away
|
||||
function handleCheckInButtonCancel() {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
|
||||
Reference in New Issue
Block a user