feat: US-013 - Frontend - Stats section and weekly summary

This commit is contained in:
Echo
2026-02-10 17:23:18 +00:00
parent b99c13a325
commit dfc2229091
2 changed files with 467 additions and 1 deletions

View File

@@ -675,6 +675,134 @@
border-color: var(--accent);
background: var(--accent-muted);
}
/* Stats section */
.stats-section {
margin-bottom: var(--space-4);
}
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.stat-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.stat-label {
font-size: var(--text-sm);
color: var(--text-muted);
font-weight: 500;
}
.stat-value {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--text-primary);
}
/* Weekly summary */
.weekly-summary {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.weekly-summary-header {
padding: var(--space-3) var(--space-4);
background: var(--bg-muted);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background var(--transition-base);
}
.weekly-summary-header:hover {
background: var(--bg-hover);
}
.weekly-summary-title {
font-size: var(--text-base);
font-weight: 600;
color: var(--text-primary);
}
.weekly-summary-chevron {
transition: transform var(--transition-base);
color: var(--text-muted);
}
.weekly-summary-chevron.expanded {
transform: rotate(180deg);
}
.weekly-summary-content {
display: none;
padding: var(--space-4);
}
.weekly-summary-content.visible {
display: block;
}
.weekly-chart {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--space-2);
height: 150px;
margin-bottom: var(--space-4);
}
.weekly-bar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
}
.weekly-bar {
width: 100%;
background: var(--accent);
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
transition: all var(--transition-base);
min-height: 4px;
}
.weekly-bar:hover {
opacity: 0.8;
}
.weekly-day-label {
font-size: var(--text-xs);
color: var(--text-muted);
font-weight: 500;
}
.weekly-stats {
display: flex;
gap: var(--space-4);
font-size: var(--text-sm);
color: var(--text-muted);
}
@media (max-width: 768px) {
.stats-row {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
@@ -754,6 +882,42 @@
</div>
</div>
<!-- Stats Section -->
<div id="statsSection" class="stats-section" style="display: none;">
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">Total Habits</div>
<div class="stat-value" id="statTotalHabits">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Avg Completion (30d)</div>
<div class="stat-value" id="statAvgCompletion">0%</div>
</div>
<div class="stat-card">
<div class="stat-label">Best Streak 🏆</div>
<div class="stat-value" id="statBestStreak">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Lives</div>
<div class="stat-value" id="statTotalLives">0</div>
</div>
</div>
<div class="weekly-summary">
<div class="weekly-summary-header" onclick="toggleWeeklySummary()">
<div class="weekly-summary-title">Weekly Summary</div>
<i data-lucide="chevron-down" class="weekly-summary-chevron" id="weeklySummaryChevron"></i>
</div>
<div class="weekly-summary-content" id="weeklySummaryContent">
<div class="weekly-chart" id="weeklyChart"></div>
<div class="weekly-stats" id="weeklyStats">
<span id="weeklyCompletedText">0 completed this week</span>
<span id="weeklySkippedText">0 skipped this week</span>
</div>
</div>
</div>
</div>
<div id="habitsContainer">
<div class="empty-state">
<i data-lucide="loader"></i>
@@ -1031,6 +1195,9 @@
function renderHabits() {
const container = document.getElementById('habitsContainer');
// Render stats section
renderStats();
if (habits.length === 0) {
container.innerHTML = `
<div class="empty-state">
@@ -1199,6 +1366,114 @@
return 'low';
}
// Stats calculation and rendering
function renderStats() {
const statsSection = document.getElementById('statsSection');
if (habits.length === 0) {
statsSection.style.display = 'none';
return;
}
statsSection.style.display = 'block';
// Calculate stats
const totalHabits = habits.length;
// Average completion rate (30d) across all habits
const avgCompletion = habits.length > 0
? Math.round(habits.reduce((sum, h) => sum + (h.completion_rate_30d || 0), 0) / habits.length)
: 0;
// Best streak across all habits
const bestStreak = Math.max(...habits.map(h => h.streak?.best || 0), 0);
// Total lives available
const totalLives = habits.reduce((sum, h) => sum + (h.lives || 0), 0);
// Update DOM
document.getElementById('statTotalHabits').textContent = totalHabits;
document.getElementById('statAvgCompletion').textContent = `${avgCompletion}%`;
document.getElementById('statBestStreak').textContent = bestStreak;
document.getElementById('statTotalLives').textContent = totalLives;
// Render weekly summary
renderWeeklySummary();
}
function renderWeeklySummary() {
const chartContainer = document.getElementById('weeklyChart');
// Get current week's data (Mon-Sun)
const today = new Date();
const currentDayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
const mondayOffset = currentDayOfWeek === 0 ? -6 : 1 - currentDayOfWeek;
const monday = new Date(today);
monday.setDate(today.getDate() + mondayOffset);
monday.setHours(0, 0, 0, 0);
// Calculate completions per day (Mon-Sun)
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const completionsPerDay = new Array(7).fill(0);
let weeklyCompleted = 0;
let weeklySkipped = 0;
habits.forEach(habit => {
(habit.completions || []).forEach(completion => {
const compDate = new Date(completion.date);
compDate.setHours(0, 0, 0, 0);
// Check if completion is in current week
const daysDiff = Math.floor((compDate - monday) / (1000 * 60 * 60 * 24));
if (daysDiff >= 0 && daysDiff < 7) {
if (completion.type === 'check') {
completionsPerDay[daysDiff]++;
weeklyCompleted++;
} else if (completion.type === 'skip') {
weeklySkipped++;
}
}
});
});
// Find max for scaling bars
const maxCompletions = Math.max(...completionsPerDay, 1);
// Render bars
let barsHtml = '';
for (let i = 0; i < 7; i++) {
const count = completionsPerDay[i];
const height = (count / maxCompletions) * 100;
barsHtml += `
<div class="weekly-bar-wrapper">
<div class="weekly-bar" style="height: ${height}%" title="${count} completed"></div>
<div class="weekly-day-label">${daysOfWeek[i]}</div>
</div>
`;
}
chartContainer.innerHTML = barsHtml;
// Update weekly stats text
document.getElementById('weeklyCompletedText').textContent = `${weeklyCompleted} completed this week`;
document.getElementById('weeklySkippedText').textContent = `${weeklySkipped} skipped this week`;
lucide.createIcons();
}
function toggleWeeklySummary() {
const content = document.getElementById('weeklySummaryContent');
const chevron = document.getElementById('weeklySummaryChevron');
if (content.classList.contains('visible')) {
content.classList.remove('visible');
chevron.classList.remove('expanded');
} else {
content.classList.add('visible');
chevron.classList.add('expanded');
}
}
// Modal state
let selectedColor = '#3B82F6';
let selectedIcon = 'dumbbell';