Files
clawd/dashboard/habits.html

974 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/echo/favicon.svg">
<title>Echo · Habit Tracker</title>
<link rel="stylesheet" href="/echo/common.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="/echo/swipe-nav.js"></script>
<style>
.main {
max-width: 900px;
margin: 0 auto;
padding: var(--space-5);
}
.page-header {
margin-bottom: var(--space-5);
}
.page-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.page-subtitle {
font-size: var(--text-sm);
color: var(--text-muted);
}
/* Empty state */
.empty-state {
text-align: center;
padding: var(--space-6) var(--space-4);
color: var(--text-muted);
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: var(--space-3);
opacity: 0.5;
color: var(--text-muted);
}
.empty-state-message {
font-size: var(--text-base);
margin-bottom: var(--space-4);
}
/* Add habit button */
.add-habit-btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.add-habit-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.add-habit-btn svg {
width: 18px;
height: 18px;
}
/* Habits list */
.habits-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.habit-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-4);
display: flex;
align-items: center;
gap: var(--space-3);
transition: all var(--transition-fast);
}
.habit-card:hover {
border-color: var(--accent);
transform: translateY(-1px);
}
.habit-card.checked {
background: rgba(34, 197, 94, 0.1);
}
.habit-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
background: var(--bg-elevated);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.habit-icon svg {
width: 20px;
height: 20px;
color: var(--accent);
}
.habit-info {
flex: 1;
}
.habit-name {
font-size: var(--text-base);
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.habit-frequency {
display: inline-block;
font-size: var(--text-xs);
color: var(--text-secondary);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: var(--radius-sm);
text-transform: capitalize;
}
.habit-streak {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-xl);
font-weight: 600;
color: var(--accent);
flex-shrink: 0;
}
/* Delete button */
.habit-delete-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg-base);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.habit-delete-btn:hover {
border-color: var(--text-danger);
background: rgba(239, 68, 68, 0.1);
}
.habit-delete-btn svg {
width: 16px;
height: 16px;
color: var(--text-muted);
}
.habit-delete-btn:hover svg {
color: var(--text-danger);
}
/* Habit checkbox */
.habit-checkbox {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
background: var(--bg-base);
}
.habit-checkbox:hover:not(.disabled) {
border-color: var(--accent);
background: var(--accent-light, rgba(99, 102, 241, 0.1));
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.habit-checkbox.checked {
background: var(--accent);
border-color: var(--accent);
}
.habit-checkbox.checked svg {
color: white;
}
.habit-checkbox.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.habit-checkbox svg {
width: 18px;
height: 18px;
color: var(--accent);
}
/* Loading state */
.loading-state {
text-align: center;
padding: var(--space-6) var(--space-4);
color: var(--text-muted);
display: none;
}
.loading-state.active {
display: block;
}
.loading-state svg {
width: 48px;
height: 48px;
margin-bottom: var(--space-3);
opacity: 0.5;
color: var(--text-muted);
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Error state */
.error-state {
text-align: center;
padding: var(--space-6) var(--space-4);
color: var(--text-danger);
display: none;
}
.error-state.active {
display: block;
}
.error-state svg {
width: 48px;
height: 48px;
margin-bottom: var(--space-3);
color: var(--text-danger);
}
/* Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-5);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.modal-title {
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: var(--space-4);
}
.form-group {
margin-bottom: var(--space-4);
}
.form-label {
display: block;
font-size: var(--text-sm);
font-weight: 500;
margin-bottom: var(--space-1);
color: var(--text-secondary);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
margin-top: var(--space-5);
}
/* Radio group */
.radio-group {
display: flex;
gap: var(--space-3);
}
.radio-option {
flex: 1;
}
.radio-option input[type="radio"] {
display: none;
}
.radio-label {
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-3);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-secondary);
}
.radio-option input[type="radio"]:checked + .radio-label {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.radio-label:hover {
border-color: var(--accent);
}
/* Toast */
.toast {
position: fixed;
bottom: var(--space-5);
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: var(--space-3) var(--space-5);
border-radius: var(--radius-md);
font-size: var(--text-sm);
opacity: 0;
transition: all var(--transition-base);
z-index: 1001;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Delete confirmation modal */
.confirm-modal {
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-5);
width: 90%;
max-width: 400px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.confirm-modal-title {
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: var(--space-3);
color: var(--text-primary);
}
.confirm-modal-message {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-5);
}
.confirm-modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
.btn-danger {
background: var(--text-danger);
color: white;
border: 1px solid var(--text-danger);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-danger:hover {
background: #dc2626;
border-color: #dc2626;
}
.btn-danger:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.main {
padding: var(--space-3);
}
.habit-card {
width: 100%;
}
.habits-list {
width: 100%;
}
.habit-icon {
width: 36px;
height: 36px;
}
.habit-icon svg {
width: 18px;
height: 18px;
}
/* Touch targets >= 44x44px for accessibility */
.habit-checkbox {
width: 44px;
height: 44px;
}
.habit-checkbox svg {
width: 20px;
height: 20px;
}
/* Full-screen modal on mobile */
.modal {
width: 100%;
max-width: 100%;
height: 100vh;
max-height: 100vh;
border-radius: 0;
padding: var(--space-5);
}
/* Larger touch targets for buttons */
.add-habit-btn {
padding: var(--space-4) var(--space-5);
min-height: 44px;
}
.btn {
min-height: 44px;
padding: var(--space-3) var(--space-5);
}
/* Larger radio buttons for touch */
.radio-label {
padding: var(--space-4);
min-height: 44px;
}
}
</style>
</head>
<body>
<header class="header">
<a href="/echo/index.html" class="logo">
<i data-lucide="circle-dot"></i>
Echo
</a>
<nav class="nav">
<a href="/echo/index.html" class="nav-item">
<i data-lucide="layout-dashboard"></i>
<span>Dashboard</span>
</a>
<a href="/echo/workspace.html" class="nav-item">
<i data-lucide="code"></i>
<span>Workspace</span>
</a>
<a href="/echo/notes.html" class="nav-item">
<i data-lucide="file-text"></i>
<span>KB</span>
</a>
<a href="/echo/files.html" class="nav-item">
<i data-lucide="folder"></i>
<span>Files</span>
</a>
<a href="/echo/habits.html" class="nav-item active">
<i data-lucide="flame"></i>
<span>Habits</span>
</a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
<i data-lucide="sun" id="themeIcon"></i>
</button>
</nav>
</header>
<main class="main">
<div class="page-header">
<h1 class="page-title">Habit Tracker</h1>
<p class="page-subtitle">Urmărește-ți obiceiurile zilnice și săptămânale</p>
</div>
<!-- Habits container -->
<div id="habitsContainer">
<!-- Loading state -->
<div class="loading-state" id="loadingState">
<i data-lucide="loader"></i>
<p>Se încarcă obiceiurile...</p>
</div>
<!-- Error state -->
<div class="error-state" id="errorState">
<i data-lucide="alert-circle"></i>
<p>Eroare la încărcarea obiceiurilor</p>
<button class="btn btn-secondary" onclick="loadHabits()">Încearcă din nou</button>
</div>
<!-- Empty state -->
<div class="empty-state" id="emptyState" style="display: none;">
<i data-lucide="target"></i>
<p class="empty-state-message">Nicio obișnuință încă. Creează prima!</p>
<button class="add-habit-btn" onclick="showAddHabitModal()">
<i data-lucide="plus"></i>
<span>Adaugă obișnuință</span>
</button>
</div>
<!-- Habits list (hidden initially) -->
<div class="habits-list" id="habitsList" style="display: none;">
<!-- Habits will be populated here by JavaScript -->
</div>
</div>
</main>
<!-- Add Habit Modal -->
<div class="modal-overlay" id="habitModal">
<div class="modal">
<h2 class="modal-title">Obișnuință nouă</h2>
<div class="form-group">
<label class="form-label">Nume *</label>
<input type="text" class="input" id="habitName" placeholder="ex: Bazin, Sală, Meditație..." autocapitalize="words" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label">Frecvență</label>
<div class="radio-group">
<div class="radio-option">
<input type="radio" name="frequency" id="freqDaily" value="daily" checked>
<label for="freqDaily" class="radio-label">Zilnic</label>
</div>
<div class="radio-option">
<input type="radio" name="frequency" id="freqWeekly" value="weekly">
<label for="freqWeekly" class="radio-label">Săptămânal</label>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="hideHabitModal()">Anulează</button>
<button class="btn btn-primary" id="habitCreateBtn" onclick="createHabit()" disabled>Creează</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal-overlay" id="deleteModal">
<div class="confirm-modal">
<h2 class="confirm-modal-title">Ștergi obișnuința?</h2>
<p class="confirm-modal-message" id="deleteModalMessage">
Ștergi obișnuința <strong id="deleteHabitName"></strong>?
</p>
<div class="confirm-modal-actions">
<button class="btn btn-secondary" onclick="hideDeleteModal()">Anulează</button>
<button class="btn btn-danger" id="confirmDeleteBtn" onclick="confirmDelete()">Șterge</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// Theme management
function initTheme() {
const saved = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', saved);
updateThemeIcon(saved);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeIcon(next);
}
function updateThemeIcon(theme) {
const icon = document.getElementById('themeIcon');
if (icon) {
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
lucide.createIcons();
}
}
// Initialize theme and icons
initTheme();
lucide.createIcons();
// Modal functions
function showAddHabitModal() {
const modal = document.getElementById('habitModal');
const nameInput = document.getElementById('habitName');
const createBtn = document.getElementById('habitCreateBtn');
// Reset form
nameInput.value = '';
document.getElementById('freqDaily').checked = true;
createBtn.disabled = true;
// Show modal
modal.classList.add('active');
nameInput.focus();
}
function hideHabitModal() {
const modal = document.getElementById('habitModal');
modal.classList.remove('active');
}
// Form validation - enable/disable Create button based on name input
document.addEventListener('DOMContentLoaded', () => {
const nameInput = document.getElementById('habitName');
const createBtn = document.getElementById('habitCreateBtn');
nameInput.addEventListener('input', () => {
const name = nameInput.value.trim();
createBtn.disabled = name.length === 0;
});
// Allow Enter key to submit if button is enabled
nameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !createBtn.disabled) {
createHabit();
}
});
});
// Create habit
async function createHabit() {
const nameInput = document.getElementById('habitName');
const createBtn = document.getElementById('habitCreateBtn');
const name = nameInput.value.trim();
const frequency = document.querySelector('input[name="frequency"]:checked').value;
if (!name) {
showToast('Te rog introdu un nume pentru obișnuință');
return;
}
// Disable button during submission (loading state)
createBtn.disabled = true;
const originalText = createBtn.textContent;
createBtn.textContent = 'Se creează...';
try {
const response = await fetch('/echo/api/habits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, frequency })
});
if (response.ok) {
// Clear input field after successful creation
nameInput.value = '';
hideHabitModal();
showToast('Obișnuință creată cu succes!');
loadHabits();
} else {
const error = await response.text();
showToast('Eroare la crearea obișnuinței: ' + error);
// Re-enable button on error (modal stays open)
createBtn.disabled = false;
createBtn.textContent = originalText;
}
} catch (error) {
showToast('Eroare la conectarea cu serverul');
console.error(error);
// Re-enable button on error (modal stays open)
createBtn.disabled = false;
createBtn.textContent = originalText;
}
}
// Toast notification
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Load habits from API
async function loadHabits() {
const loadingState = document.getElementById('loadingState');
const errorState = document.getElementById('errorState');
const emptyState = document.getElementById('emptyState');
const habitsList = document.getElementById('habitsList');
// Show loading state
loadingState.classList.add('active');
errorState.classList.remove('active');
emptyState.style.display = 'none';
habitsList.style.display = 'none';
try {
const response = await fetch('/echo/api/habits');
if (!response.ok) {
throw new Error('Failed to fetch habits');
}
const data = await response.json();
const habits = data.habits || [];
// Hide loading state
loadingState.classList.remove('active');
// Sort habits by streak descending (highest first)
habits.sort((a, b) => (b.streak || 0) - (a.streak || 0));
if (habits.length === 0) {
// Show empty state
emptyState.style.display = 'block';
} else {
// Render habits list
habitsList.innerHTML = '';
habits.forEach(habit => {
const card = createHabitCard(habit);
habitsList.appendChild(card);
});
habitsList.style.display = 'flex';
}
// Re-initialize Lucide icons
lucide.createIcons();
} catch (error) {
console.error('Error loading habits:', error);
loadingState.classList.remove('active');
errorState.classList.add('active');
}
}
// Create habit card element
function createHabitCard(habit) {
const card = document.createElement('div');
const isChecked = habit.checkedToday || false;
card.className = isChecked ? 'habit-card checked' : 'habit-card';
// Determine icon based on frequency
const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock';
// Checkbox state
const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox';
const checkIcon = isChecked ? '<i data-lucide="check"></i>' : '';
// Create card HTML
card.innerHTML = `
<div class="${checkboxClass}" data-habit-id="${habit.id}" onclick="checkHabit('${habit.id}', this)">
${checkIcon}
</div>
<div class="habit-icon">
<i data-lucide="${iconName}"></i>
</div>
<div class="habit-info">
<div class="habit-name">${escapeHtml(habit.name)}</div>
<div class="habit-frequency">${habit.frequency === 'daily' ? 'Zilnic' : 'Săptămânal'}</div>
</div>
<div class="habit-streak">
<span id="streak-${habit.id}">${habit.streak || 0}</span>
<span>🔥</span>
</div>
<button class="habit-delete-btn" onclick="showDeleteModal('${habit.id}', '${escapeHtml(habit.name).replace(/'/g, "&#39;")}')">
<i data-lucide="trash-2"></i>
</button>
`;
return card;
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Check habit (mark as done for today)
async function checkHabit(habitId, checkboxElement) {
// Don't allow checking if already checked
if (checkboxElement.classList.contains('disabled')) {
return;
}
// Optimistic UI update
checkboxElement.classList.add('checked', 'disabled');
checkboxElement.innerHTML = '<i data-lucide="check"></i>';
lucide.createIcons();
// Add 'checked' class to parent card for green background
const card = checkboxElement.closest('.habit-card');
if (card) {
card.classList.add('checked');
}
// Store original state for rollback
const originalCheckbox = checkboxElement.cloneNode(true);
const streakElement = document.getElementById(`streak-${habitId}`);
const originalStreak = streakElement ? streakElement.textContent : '0';
try {
const response = await fetch(`/echo/api/habits/${habitId}/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error('Failed to check habit');
}
const data = await response.json();
// Update streak with server value
if (streakElement && data.habit && data.habit.streak !== undefined) {
streakElement.textContent = data.habit.streak;
}
showToast('Obișnuință bifată! 🎉');
} catch (error) {
console.error('Error checking habit:', error);
// Revert checkbox on error
checkboxElement.classList.remove('checked', 'disabled');
checkboxElement.innerHTML = '';
// Revert card background
const card = checkboxElement.closest('.habit-card');
if (card) {
card.classList.remove('checked');
}
// Revert streak
if (streakElement) {
streakElement.textContent = originalStreak;
}
showToast('Eroare la bifarea obișnuinței. Încearcă din nou.');
}
}
// Delete habit functions
let habitToDelete = null;
function showDeleteModal(habitId, habitName) {
habitToDelete = habitId;
const modal = document.getElementById('deleteModal');
const nameElement = document.getElementById('deleteHabitName');
// Decode HTML entities for display
const tempDiv = document.createElement('div');
tempDiv.innerHTML = habitName;
nameElement.textContent = tempDiv.textContent;
modal.classList.add('active');
}
function hideDeleteModal() {
const modal = document.getElementById('deleteModal');
modal.classList.remove('active');
habitToDelete = null;
}
async function confirmDelete() {
if (!habitToDelete) {
return;
}
const deleteBtn = document.getElementById('confirmDeleteBtn');
// Disable button during deletion
deleteBtn.disabled = true;
const originalText = deleteBtn.textContent;
deleteBtn.textContent = 'Se șterge...';
try {
const response = await fetch(`/echo/api/habits/${habitToDelete}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error('Failed to delete habit');
}
hideDeleteModal();
showToast('Obișnuință ștearsă cu succes');
loadHabits();
} catch (error) {
console.error('Error deleting habit:', error);
showToast('Eroare la ștergerea obișnuinței. Încearcă din nou.');
// Re-enable button on error
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
}
}
// Load habits on page load
document.addEventListener('DOMContentLoaded', () => {
loadHabits();
});
</script>
</body>
</html>