feat: US-007 - Frontend - Habit card component

This commit is contained in:
Echo
2026-02-10 16:28:08 +00:00
parent f889e69b54
commit b99133de79
2 changed files with 437 additions and 11 deletions

View File

@@ -79,13 +79,17 @@
opacity: 0.7;
}
/* Habit card (placeholder for next story) */
/* Habit card */
.habit-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
border-left: 4px solid var(--accent);
padding: var(--space-4);
transition: all var(--transition-base);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.habit-card:hover {
@@ -94,17 +98,157 @@
box-shadow: var(--shadow-md);
}
.habit-name {
.habit-card-header {
display: flex;
align-items: center;
gap: var(--space-2);
}
.habit-card-icon {
width: 20px;
height: 20px;
color: var(--text-primary);
flex-shrink: 0;
}
.habit-card-name {
flex: 1;
font-size: var(--text-base);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.habit-meta {
.habit-card-actions {
display: flex;
gap: var(--space-2);
}
.habit-card-action-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: var(--space-1);
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
transition: all var(--transition-base);
}
.habit-card-action-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.habit-card-action-btn svg {
width: 16px;
height: 16px;
}
.habit-card-streaks {
display: flex;
gap: var(--space-4);
font-size: var(--text-sm);
color: var(--text-muted);
}
.habit-card-streak {
display: flex;
align-items: center;
gap: var(--space-1);
}
.habit-card-check-btn {
width: 100%;
padding: var(--space-3);
border: 2px solid var(--accent);
background: var(--accent);
color: white;
border-radius: var(--radius-md);
font-size: var(--text-base);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
}
.habit-card-check-btn:hover:not(:disabled) {
background: var(--accent-hover);
transform: scale(1.02);
}
.habit-card-check-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--bg-muted);
border-color: var(--border);
color: var(--text-muted);
}
.habit-card-last-check {
font-size: var(--text-sm);
color: var(--text-muted);
text-align: center;
}
.habit-card-lives {
display: flex;
justify-content: center;
gap: var(--space-1);
font-size: var(--text-lg);
}
.habit-card-completion {
font-size: var(--text-sm);
color: var(--text-muted);
text-align: center;
}
.habit-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: var(--space-2);
border-top: 1px solid var(--border);
}
.habit-card-category {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
background: var(--bg-muted);
color: var(--text-muted);
}
.habit-card-priority {
font-size: var(--text-xs);
color: var(--text-muted);
display: flex;
align-items: center;
gap: var(--space-1);
}
.priority-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.priority-high {
background: var(--error);
}
.priority-medium {
background: var(--warning);
}
.priority-low {
background: var(--success);
}
</style>
</head>
<body>
@@ -222,24 +366,134 @@
lucide.createIcons();
}
// Render single habit card (placeholder - full card in next story)
// Render single habit card
function renderHabitCard(habit) {
const isDoneToday = isCheckedToday(habit);
const lastCheckInfo = getLastCheckInfo(habit);
const livesHtml = renderLives(habit.lives || 3);
const completionRate = habit.completion_rate_30d || 0;
return `
<div class="habit-card">
<div class="habit-name">${escapeHtml(habit.name)}</div>
<div class="habit-meta">
Frequency: ${habit.frequency.type}
${habit.category ? ` · ${habit.category}` : ''}
<div class="habit-card" style="border-left-color: ${habit.color}">
<div class="habit-card-header">
<i data-lucide="${habit.icon || 'circle'}" class="habit-card-icon"></i>
<span class="habit-card-name">${escapeHtml(habit.name)}</span>
<div class="habit-card-actions">
<button class="habit-card-action-btn" onclick="showEditHabitModal('${habit.id}')" title="Edit">
<i data-lucide="settings"></i>
</button>
<button class="habit-card-action-btn" onclick="deleteHabit('${habit.id}')" title="Delete">
<i data-lucide="trash-2"></i>
</button>
</div>
</div>
<div class="habit-card-streaks">
<div class="habit-card-streak">
🔥 ${habit.streak?.current || 0}
</div>
<div class="habit-card-streak">
🏆 ${habit.streak?.best || 0}
</div>
</div>
<button
class="habit-card-check-btn"
onclick="checkInHabit('${habit.id}')"
${isDoneToday ? 'disabled' : ''}
>
${isDoneToday ? '✓ Done today' : 'Check In'}
</button>
<div class="habit-card-last-check">${lastCheckInfo}</div>
<div class="habit-card-lives">${livesHtml}</div>
<div class="habit-card-completion">${completionRate}% (30d)</div>
<div class="habit-card-footer">
<span class="habit-card-category">${escapeHtml(habit.category || 'General')}</span>
<span class="habit-card-priority">
<span class="priority-indicator priority-${getPriorityLevel(habit.priority || 3)}"></span>
P${habit.priority || 3}
</span>
</div>
</div>
`;
}
// Check if habit was checked today
function isCheckedToday(habit) {
if (!habit.completions || habit.completions.length === 0) {
return false;
}
const today = new Date().toISOString().split('T')[0];
return habit.completions.some(c => c.date === today);
}
// Get last check-in info text
function getLastCheckInfo(habit) {
if (!habit.completions || habit.completions.length === 0) {
return 'Last: Never';
}
const lastCompletion = habit.completions[habit.completions.length - 1];
const lastDate = new Date(lastCompletion.date);
const today = new Date();
today.setHours(0, 0, 0, 0);
lastDate.setHours(0, 0, 0, 0);
const diffDays = Math.floor((today - lastDate) / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return 'Last: Today';
} else if (diffDays === 1) {
return 'Last: Yesterday';
} else {
return `Last: ${diffDays} days ago`;
}
}
// Render lives as hearts
function renderLives(lives) {
const totalLives = 3;
let html = '';
for (let i = 0; i < totalLives; i++) {
html += i < lives ? '❤️' : '🖤';
}
return html;
}
// Get priority level string
function getPriorityLevel(priority) {
if (priority === 1) return 'high';
if (priority === 2) return 'medium';
return 'low';
}
// Show add habit modal (placeholder - full modal in next stories)
function showAddHabitModal() {
alert('Add Habit modal - coming in next story!');
}
// Show edit habit modal (placeholder)
function showEditHabitModal(habitId) {
alert('Edit Habit modal - coming in next story!');
}
// Delete habit (placeholder)
async function deleteHabit(habitId) {
if (!confirm('Are you sure you want to delete this habit?')) {
return;
}
alert('Delete functionality - coming in next story!');
}
// Check in habit (placeholder)
async function checkInHabit(habitId) {
alert('Check-in functionality - coming in next story!');
}
// Show error message
function showError(message) {
const container = document.getElementById('habitsContainer');